diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..bd8e261 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile:1 +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libxkbcommon0 \ + ca-certificates \ + ca-certificates-java \ + make \ + curl \ + git \ + openjdk-17-jdk-headless \ + unzip \ + libc++1 \ + vim \ + && apt-get clean autoclean + +# Ensure UTF-8 encoding +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 + +WORKDIR /workspace + +COPY . /workspace diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..d55fc4d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Debian", + "build": { + "dockerfile": "Dockerfile" + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..022b841 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b11acac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,109 @@ +name: CI +on: + push: + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +jobs: + lint: + timeout-minutes: 15 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/stagehand-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: | + 8 + 21 + cache: gradle + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 + + - name: Run lints + run: ./scripts/lint + + build: + timeout-minutes: 15 + name: build + permissions: + contents: read + id-token: write + runs-on: ${{ github.repository == 'stainless-sdks/stagehand-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: | + 8 + 21 + cache: gradle + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3 + + - name: Build SDK + run: ./scripts/build + + - name: Get GitHub OIDC Token + if: |- + github.repository == 'stainless-sdks/stagehand-java' && + !startsWith(github.ref, 'refs/heads/stl/') + id: github-oidc + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Build and upload Maven artifacts + if: |- + github.repository == 'stainless-sdks/stagehand-java' && + !startsWith(github.ref, 'refs/heads/stl/') + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + PROJECT: stagehand-java + run: ./scripts/upload-artifacts + test: + timeout-minutes: 15 + name: test + runs-on: ${{ github.repository == 'stainless-sdks/stagehand-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: | + 8 + 21 + cache: gradle + + - name: Set up Gradle + uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + + - name: Run tests + run: ./scripts/test diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml new file mode 100644 index 0000000..ebab6f0 --- /dev/null +++ b/.github/workflows/publish-sonatype.yml @@ -0,0 +1,41 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to Sonatype in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/browserbase/stagehand-java/actions/workflows/publish-sonatype.yml +name: Publish Sonatype +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: temurin + java-version: | + 8 + 21 + cache: gradle + + - name: Set up Gradle + uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0 + + - name: Publish to Sonatype + run: |- + export -- GPG_SIGNING_KEY_ID + printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD" + GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')" + ./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache + env: + SONATYPE_USERNAME: ${{ secrets.STAGEHAND_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.STAGEHAND_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }} + GPG_SIGNING_KEY: ${{ secrets.STAGEHAND_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }} + GPG_SIGNING_PASSWORD: ${{ secrets.STAGEHAND_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..4fcd85e --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,24 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'browserbase/stagehand-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + SONATYPE_USERNAME: ${{ secrets.STAGEHAND_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.STAGEHAND_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }} + GPG_SIGNING_KEY: ${{ secrets.STAGEHAND_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }} + GPG_SIGNING_PASSWORD: ${{ secrets.STAGEHAND_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90b85e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.prism.log +.stdy.log +.gradle +.idea +.kotlin +build/ +codegen.log +kls_database.db diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..d11c8fc --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "3.20.0" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 0000000..0339c57 --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-eae8400fade7b2c8329c4148f56de92e147c34c0feecb420c015aab6544a9acc.yml +openapi_spec_hash: 0a9eff1ac1d464e89cbd9db64709b08a +config_hash: 1fb12ae9b478488bc1e56bfbdc210b01 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a7b82c2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2026 stagehand + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index eaf1a54..e2281bf 100644 --- a/README.md +++ b/README.md @@ -1 +1,786 @@ -# stagehand-java \ No newline at end of file +# Stagehand Java API Library + + + +[![Maven Central](https://img.shields.io/maven-central/v/com.browserbase.api/stagehand-java)](https://central.sonatype.com/artifact/com.browserbase.api/stagehand-java/3.20.0) +[![javadoc](https://javadoc.io/badge2/com.browserbase.api/stagehand-java/3.20.0/javadoc.svg)](https://javadoc.io/doc/com.browserbase.api/stagehand-java/3.20.0) + + + +The Stagehand Java SDK provides convenient access to the [Stagehand REST API](https://docs.stagehand.dev) from applications written in Java. + +The Stagehand Java SDK is similar to the Stagehand Kotlin SDK but with minor differences that make it more ergonomic for use in Java, such as `Optional` instead of nullable values, `Stream` instead of `Sequence`, and `CompletableFuture` instead of suspend functions. + +It is generated with [Stainless](https://www.stainless.com/). + + + +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/3.20.0). + + + +## Installation + + + +### Gradle + +```kotlin +implementation("com.browserbase.api:stagehand-java:3.20.0") +``` + +### Maven + +```xml + + com.browserbase.api + stagehand-java + 3.20.0 + +``` + + + +## Requirements + +This library requires Java 8 or later. + +## Usage + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; +import com.browserbase.api.models.sessions.SessionActParams; +import com.browserbase.api.models.sessions.SessionActResponse; + +// Configures using the `stagehand.browserbaseApiKey`, `stagehand.browserbaseProjectId`, `stagehand.modelApiKey` and `stagehand.baseUrl` system properties +// Or configures using the `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `MODEL_API_KEY` and `STAGEHAND_BASE_URL` environment variables +StagehandClient client = StagehandOkHttpClient.fromEnv(); + +SessionActParams params = SessionActParams.builder() + .id("00000000-your-session-id-000000000000") + .input("click the first link on the page") + .build(); +SessionActResponse response = client.sessions().act(params); +``` + +## Client configuration + +Configure the client using system properties or environment variables: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; + +// Configures using the `stagehand.browserbaseApiKey`, `stagehand.browserbaseProjectId`, `stagehand.modelApiKey` and `stagehand.baseUrl` system properties +// Or configures using the `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `MODEL_API_KEY` and `STAGEHAND_BASE_URL` environment variables +StagehandClient client = StagehandOkHttpClient.fromEnv(); +``` + +Or manually: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; + +StagehandClient client = StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build(); +``` + +Or using a combination of the two approaches: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; + +StagehandClient client = StagehandOkHttpClient.builder() + // Configures using the `stagehand.browserbaseApiKey`, `stagehand.browserbaseProjectId`, `stagehand.modelApiKey` and `stagehand.baseUrl` system properties + // Or configures using the `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `MODEL_API_KEY` and `STAGEHAND_BASE_URL` environment variables + .fromEnv() + .browserbaseApiKey("My Browserbase API Key") + .build(); +``` + +See this table for the available options: + +| Setter | System property | Environment variable | Required | Default value | +| ---------------------- | -------------------------------- | ------------------------ | -------- | ----------------------------------------- | +| `browserbaseApiKey` | `stagehand.browserbaseApiKey` | `BROWSERBASE_API_KEY` | true | - | +| `browserbaseProjectId` | `stagehand.browserbaseProjectId` | `BROWSERBASE_PROJECT_ID` | false | - | +| `modelApiKey` | `stagehand.modelApiKey` | `MODEL_API_KEY` | true | - | +| `baseUrl` | `stagehand.baseUrl` | `STAGEHAND_BASE_URL` | true | `"https://api.stagehand.browserbase.com"` | + +System properties take precedence over environment variables. + +> [!TIP] +> Don't create more than one client in the same application. Each client has a connection pool and +> thread pools, which are more efficient to share between requests. + +### Modifying configuration + +To temporarily use a modified client configuration, while reusing the same connection and thread pools, call `withOptions()` on any client or service: + +```java +import com.browserbase.api.client.StagehandClient; + +StagehandClient clientWithOptions = client.withOptions(optionsBuilder -> { + optionsBuilder.baseUrl("https://example.com"); + optionsBuilder.maxRetries(42); +}); +``` + +The `withOptions()` method does not affect the original client or service. + +## Requests and responses + +To send a request to the Stagehand API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class. + +For example, `client.sessions().act(...)` should be called with an instance of `SessionActParams`, and it will return an instance of `SessionActResponse`. + +## Immutability + +Each class in the SDK has an associated [builder](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) or factory method for constructing it. + +Each class is [immutable](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html) once constructed. If the class has an associated builder, then it has a `toBuilder()` method, which can be used to convert it back to a builder for making a modified copy. + +Because each class is immutable, builder modification will _never_ affect already built class instances. + +## Asynchronous execution + +The default client is synchronous. To switch to asynchronous execution, call the `async()` method: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; +import com.browserbase.api.models.sessions.SessionActParams; +import com.browserbase.api.models.sessions.SessionActResponse; +import java.util.concurrent.CompletableFuture; + +// Configures using the `stagehand.browserbaseApiKey`, `stagehand.browserbaseProjectId`, `stagehand.modelApiKey` and `stagehand.baseUrl` system properties +// Or configures using the `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `MODEL_API_KEY` and `STAGEHAND_BASE_URL` environment variables +StagehandClient client = StagehandOkHttpClient.fromEnv(); + +SessionActParams params = SessionActParams.builder() + .id("00000000-your-session-id-000000000000") + .input("click the first link on the page") + .build(); +CompletableFuture response = client.async().sessions().act(params); +``` + +Or create an asynchronous client from the beginning: + +```java +import com.browserbase.api.client.StagehandClientAsync; +import com.browserbase.api.client.okhttp.StagehandOkHttpClientAsync; +import com.browserbase.api.models.sessions.SessionActParams; +import com.browserbase.api.models.sessions.SessionActResponse; +import java.util.concurrent.CompletableFuture; + +// Configures using the `stagehand.browserbaseApiKey`, `stagehand.browserbaseProjectId`, `stagehand.modelApiKey` and `stagehand.baseUrl` system properties +// Or configures using the `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `MODEL_API_KEY` and `STAGEHAND_BASE_URL` environment variables +StagehandClientAsync client = StagehandOkHttpClientAsync.fromEnv(); + +SessionActParams params = SessionActParams.builder() + .id("00000000-your-session-id-000000000000") + .input("click the first link on the page") + .build(); +CompletableFuture response = client.sessions().act(params); +``` + +The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s. + +## Streaming + +The SDK defines methods that return response "chunk" streams, where each chunk can be individually processed as soon as it arrives instead of waiting on the full response. Streaming methods generally correspond to [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) or [JSONL](https://jsonlines.org) responses. + +Some of these methods may have streaming and non-streaming variants, but a streaming method will always have a `Streaming` suffix in its name, even if it doesn't have a non-streaming variant. + +These streaming methods return [`StreamResponse`](stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/StreamResponse.kt) for synchronous clients: + +```java +import com.browserbase.api.core.http.StreamResponse; +import com.browserbase.api.models.sessions.StreamEvent; + +try (StreamResponse streamResponse = client.sessions().actStreaming(params)) { + streamResponse.stream().forEach(chunk -> { + System.out.println(chunk); + }); + System.out.println("No more chunks!"); +} +``` + +Or [`AsyncStreamResponse`](stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/AsyncStreamResponse.kt) for asynchronous clients: + +```java +import com.browserbase.api.core.http.AsyncStreamResponse; +import com.browserbase.api.models.sessions.StreamEvent; +import java.util.Optional; + +client.async().sessions().actStreaming(params).subscribe(chunk -> { + System.out.println(chunk); +}); + +// If you need to handle errors or completion of the stream +client.async().sessions().actStreaming(params).subscribe(new AsyncStreamResponse.Handler<>() { + @Override + public void onNext(StreamEvent chunk) { + System.out.println(chunk); + } + + @Override + public void onComplete(Optional error) { + if (error.isPresent()) { + System.out.println("Something went wrong!"); + throw new RuntimeException(error.get()); + } else { + System.out.println("No more chunks!"); + } + } +}); + +// Or use futures +client.async().sessions().actStreaming(params) + .subscribe(chunk -> { + System.out.println(chunk); + }) + .onCompleteFuture() + .whenComplete((unused, error) -> { + if (error != null) { + System.out.println("Something went wrong!"); + throw new RuntimeException(error); + } else { + System.out.println("No more chunks!"); + } + }); +``` + +Async streaming uses a dedicated per-client cached thread pool [`Executor`](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) to stream without blocking the current thread. This default is suitable for most purposes. + +To use a different `Executor`, configure the subscription using the `executor` parameter: + +```java +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +Executor executor = Executors.newFixedThreadPool(4); +client.async().sessions().actStreaming(params).subscribe( + chunk -> System.out.println(chunk), executor +); +``` + +Or configure the client globally using the `streamHandlerExecutor` method: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; +import java.util.concurrent.Executors; + +StagehandClient client = StagehandOkHttpClient.builder() + .fromEnv() + .streamHandlerExecutor(Executors.newFixedThreadPool(4)) + .build(); +``` + +## Raw responses + +The SDK defines methods that deserialize responses into instances of Java classes. However, these methods don't provide access to the response headers, status code, or the raw response body. + +To access this data, prefix any HTTP method call on a client or service with `withRawResponse()`: + +```java +import com.browserbase.api.core.http.Headers; +import com.browserbase.api.core.http.HttpResponseFor; +import com.browserbase.api.models.sessions.SessionStartParams; +import com.browserbase.api.models.sessions.SessionStartResponse; + +SessionStartParams params = SessionStartParams.builder() + .modelName("openai/gpt-5.4-mini") + .build(); +HttpResponseFor response = client.sessions().withRawResponse().start(params); + +int statusCode = response.statusCode(); +Headers headers = response.headers(); +``` + +You can still deserialize the response into an instance of a Java class if needed: + +```java +import com.browserbase.api.models.sessions.SessionStartResponse; + +SessionStartResponse parsedResponse = response.parse(); +``` + +## Error handling + +The SDK throws custom unchecked exception types: + +- [`StagehandServiceException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code: + + | Status | Exception | + | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | + | 400 | [`BadRequestException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/BadRequestException.kt) | + | 401 | [`UnauthorizedException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnauthorizedException.kt) | + | 403 | [`PermissionDeniedException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/PermissionDeniedException.kt) | + | 404 | [`NotFoundException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/NotFoundException.kt) | + | 422 | [`UnprocessableEntityException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnprocessableEntityException.kt) | + | 429 | [`RateLimitException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/RateLimitException.kt) | + | 5xx | [`InternalServerException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/InternalServerException.kt) | + | others | [`UnexpectedStatusCodeException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnexpectedStatusCodeException.kt) | + + [`SseException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/SseException.kt) is thrown for errors encountered during [SSE streaming](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) after a successful initial HTTP response. + +- [`StagehandIoException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandIoException.kt): I/O networking errors. + +- [`StagehandRetryableException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandRetryableException.kt): Generic error indicating a failure that could be retried by the client. + +- [`StagehandInvalidDataException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response. + +- [`StagehandException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class. + +## Logging + +Enable logging by setting the `STAGEHAND_LOG` environment variable to `info`: + +```sh +export STAGEHAND_LOG=info +``` + +Or to `debug` for more verbose logging: + +```sh +export STAGEHAND_LOG=debug +``` + +Or configure the client manually using the `logLevel` method: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; +import com.browserbase.api.core.LogLevel; + +StagehandClient client = StagehandOkHttpClient.builder() + .fromEnv() + .logLevel(LogLevel.INFO) + .build(); +``` + +## ProGuard and R8 + +Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `stagehand-java-core` is published with a [configuration file](stagehand-java-core/src/main/resources/META-INF/proguard/stagehand-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage). + +ProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary. + +## Jackson + +The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default. + +The SDK throws an exception if it detects an incompatible Jackson version at runtime (e.g. if the default version was overridden in your Maven or Gradle config). + +If the SDK threw an exception, but you're _certain_ the version is compatible, then disable the version check using the `checkJacksonVersionCompatibility` on [`StagehandOkHttpClient`](stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt) or [`StagehandOkHttpClientAsync`](stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt). + +> [!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 + +The SDK automatically retries 2 times by default, with a short exponential backoff between requests. + +Only the following error types are retried: + +- Connection errors (for example, due to a network connectivity problem) +- 408 Request Timeout +- 409 Conflict +- 429 Rate Limit +- 5xx Internal + +The API may also explicitly instruct the SDK to retry or not retry a request. + +To set a custom number of retries, configure the client using the `maxRetries` method: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; + +StagehandClient client = StagehandOkHttpClient.builder() + .fromEnv() + .maxRetries(4) + .build(); +``` + +### Timeouts + +Requests time out after 1 minute by default. + +To set a custom timeout, configure the method call using the `timeout` method: + +```java +import com.browserbase.api.models.sessions.SessionStartResponse; + +SessionStartResponse response = client.sessions().start( + params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build() +); +``` + +Or configure the default for all method calls at the client level: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; +import java.time.Duration; + +StagehandClient client = StagehandOkHttpClient.builder() + .fromEnv() + .timeout(Duration.ofSeconds(30)) + .build(); +``` + +### Proxies + +To route requests through a proxy, configure the client using the `proxy` method: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; +import java.net.InetSocketAddress; +import java.net.Proxy; + +StagehandClient client = StagehandOkHttpClient.builder() + .fromEnv() + .proxy(new Proxy( + Proxy.Type.HTTP, new InetSocketAddress( + "https://example.com", 8080 + ) + )) + .build(); +``` + +If the proxy responds with `407 Proxy Authentication Required`, supply credentials by also configuring `proxyAuthenticator`: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; +import com.browserbase.api.core.http.ProxyAuthenticator; + +StagehandClient client = StagehandOkHttpClient.builder() + .fromEnv() + .proxy(...) + // Or a custom implementation of `ProxyAuthenticator`. + .proxyAuthenticator(ProxyAuthenticator.basic("username", "password")) + .build(); +``` + +### Connection pooling + +To customize the underlying OkHttp connection pool, configure the client using the `maxIdleConnections` and `keepAliveDuration` methods: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; +import java.time.Duration; + +StagehandClient client = StagehandOkHttpClient.builder() + .fromEnv() + // If `maxIdleConnections` is set, then `keepAliveDuration` must be set, and vice versa. + .maxIdleConnections(10) + .keepAliveDuration(Duration.ofMinutes(2)) + .build(); +``` + +If both options are unset, OkHttp's default connection pool settings are used. + +### HTTPS + +> [!NOTE] +> Most applications should not call these methods, and instead use the system defaults. The defaults include +> special optimizations that can be lost if the implementations are modified. + +To configure how HTTPS connections are secured, configure the client using the `sslSocketFactory`, `trustManager`, and `hostnameVerifier` methods: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; + +StagehandClient client = StagehandOkHttpClient.builder() + .fromEnv() + // If `sslSocketFactory` is set, then `trustManager` must be set, and vice versa. + .sslSocketFactory(yourSSLSocketFactory) + .trustManager(yourTrustManager) + .hostnameVerifier(yourHostnameVerifier) + .build(); +``` + +### Custom HTTP client + +The SDK consists of three artifacts: + +- `stagehand-java-core` + - Contains core SDK logic + - Does not depend on [OkHttp](https://square.github.io/okhttp) + - Exposes [`StagehandClient`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClient.kt), [`StagehandClientAsync`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsync.kt), [`StagehandClientImpl`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt), and [`StagehandClientAsyncImpl`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt), all of which can work with any HTTP client +- `stagehand-java-client-okhttp` + - Depends on [OkHttp](https://square.github.io/okhttp) + - Exposes [`StagehandOkHttpClient`](stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt) and [`StagehandOkHttpClientAsync`](stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt), which provide a way to construct [`StagehandClientImpl`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt) and [`StagehandClientAsyncImpl`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt), respectively, using OkHttp +- `stagehand-java` + - Depends on and exposes the APIs of both `stagehand-java-core` and `stagehand-java-client-okhttp` + - Does not have its own logic + +This structure allows replacing the SDK's default HTTP client without pulling in unnecessary dependencies. + +#### Customized [`OkHttpClient`](https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.html) + +> [!TIP] +> Try the available [network options](#network-options) before replacing the default client. + +To use a customized `OkHttpClient`: + +1. Replace your [`stagehand-java` dependency](#installation) with `stagehand-java-core` +2. Copy `stagehand-java-client-okhttp`'s [`OkHttpClient`](stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt) class into your code and customize it +3. Construct [`StagehandClientImpl`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt) or [`StagehandClientAsyncImpl`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt), similarly to [`StagehandOkHttpClient`](stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt) or [`StagehandOkHttpClientAsync`](stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt), using your customized client + +### Completely custom HTTP client + +To use a completely custom HTTP client: + +1. Replace your [`stagehand-java` dependency](#installation) with `stagehand-java-core` +2. Write a class that implements the [`HttpClient`](stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpClient.kt) interface +3. Construct [`StagehandClientImpl`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt) or [`StagehandClientAsyncImpl`](stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt), similarly to [`StagehandOkHttpClient`](stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt) or [`StagehandOkHttpClientAsync`](stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt), using your new client class + +## Undocumented API functionality + +The SDK is typed for convenient usage of the documented API. However, it also supports working with undocumented or not yet supported parts of the API. + +### Parameters + +To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQueryParam`, or `putAdditionalBodyProperty` methods on any `Params` class: + +```java +import com.browserbase.api.core.JsonValue; +import com.browserbase.api.models.sessions.SessionActParams; + +SessionActParams params = SessionActParams.builder() + .putAdditionalHeader("Secret-Header", "42") + .putAdditionalQueryParam("secret_query_param", "42") + .putAdditionalBodyProperty("secretProperty", JsonValue.from("42")) + .build(); +``` + +These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods. + +To set undocumented parameters on _nested_ headers, query params, or body classes, call the `putAdditionalProperty` method on the nested class: + +```java +import com.browserbase.api.core.JsonValue; +import com.browserbase.api.models.sessions.SessionActParams; + +SessionActParams params = SessionActParams.builder() + .options(SessionActParams.Options.builder() + .putAdditionalProperty("secretProperty", JsonValue.from("42")) + .build()) + .build(); +``` + +These properties can be accessed on the nested built object later using the `_additionalProperties()` method. + +To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Values.kt) object to its setter: + +```java +import com.browserbase.api.core.JsonValue; +import com.browserbase.api.models.sessions.SessionActParams; + +SessionActParams params = SessionActParams.builder() + .input(JsonValue.from(42)) + .build(); +``` + +The most straightforward way to create a [`JsonValue`](stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Values.kt) is using its `from(...)` method: + +```java +import com.browserbase.api.core.JsonValue; +import java.util.List; +import java.util.Map; + +// Create primitive JSON values +JsonValue nullValue = JsonValue.from(null); +JsonValue booleanValue = JsonValue.from(true); +JsonValue numberValue = JsonValue.from(42); +JsonValue stringValue = JsonValue.from("Hello World!"); + +// Create a JSON array value equivalent to `["Hello", "World"]` +JsonValue arrayValue = JsonValue.from(List.of( + "Hello", "World" +)); + +// Create a JSON object value equivalent to `{ "a": 1, "b": 2 }` +JsonValue objectValue = JsonValue.from(Map.of( + "a", 1, + "b", 2 +)); + +// Create an arbitrarily nested JSON equivalent to: +// { +// "a": [1, 2], +// "b": [3, 4] +// } +JsonValue complexValue = JsonValue.from(Map.of( + "a", List.of( + 1, 2 + ), + "b", List.of( + 3, 4 + ) +)); +``` + +Normally a `Builder` class's `build` method will throw [`IllegalStateException`](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html) if any required parameter or property is unset. + +To forcibly omit a required parameter or property, pass [`JsonMissing`](stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Values.kt): + +```java +import com.browserbase.api.core.JsonMissing; +import com.browserbase.api.models.sessions.SessionActParams; + +SessionActParams params = SessionActParams.builder() + .input("Click the login button") + .id(JsonMissing.of()) + .build(); +``` + +### Response properties + +To access undocumented response properties, call the `_additionalProperties()` method: + +```java +import com.browserbase.api.core.JsonValue; +import java.util.Map; + +Map additionalProperties = client.sessions().act(params)._additionalProperties(); +JsonValue secretPropertyValue = additionalProperties.get("secretProperty"); + +String result = secretPropertyValue.accept(new JsonValue.Visitor<>() { + @Override + public String visitNull() { + return "It's null!"; + } + + @Override + public String visitBoolean(boolean value) { + return "It's a boolean!"; + } + + @Override + public String visitNumber(Number value) { + return "It's a number!"; + } + + // Other methods include `visitMissing`, `visitString`, `visitArray`, and `visitObject` + // The default implementation of each unimplemented method delegates to `visitDefault`, which throws by default, but can also be overridden +}); +``` + +To access a property's raw JSON value, which may be undocumented, call its `_` prefixed method: + +```java +import com.browserbase.api.core.JsonField; +import com.browserbase.api.models.sessions.SessionActParams; +import java.util.Optional; + +JsonField input = client.sessions().act(params)._input(); + +if (input.isMissing()) { + // The property is absent from the JSON response +} else if (input.isNull()) { + // The property was set to literal null +} else { + // Check if value was provided as a string + // Other methods include `asNumber()`, `asBoolean()`, etc. + Optional jsonString = input.asString(); + + // Try to deserialize into a custom type + MyClass myObject = input.asUnknown().orElseThrow().convert(MyClass.class); +} +``` + +### Response validation + +In rare cases, the API may return a response that doesn't match the expected type. For example, the SDK may expect a property to contain a `String`, but the API could return something else. + +By default, the SDK will not throw an exception in this case. It will throw [`StagehandInvalidDataException`](stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandInvalidDataException.kt) only if you directly access the property. + +Validating the response is _not_ forwards compatible with new types from the API for existing fields. + +If you would still prefer to check that the response is completely well-typed upfront, then either call `validate()`: + +```java +import com.browserbase.api.models.sessions.SessionActResponse; + +SessionActResponse response = client.sessions().act(params).validate(); +``` + +Or configure the method call to validate the response using the `responseValidation` method: + +```java +import com.browserbase.api.models.sessions.SessionActResponse; + +SessionActResponse response = client.sessions().act( + params, RequestOptions.builder().responseValidation(true).build() +); +``` + +Or configure the default for all method calls at the client level: + +```java +import com.browserbase.api.client.StagehandClient; +import com.browserbase.api.client.okhttp.StagehandOkHttpClient; + +StagehandClient client = StagehandOkHttpClient.builder() + .fromEnv() + .responseValidation(true) + .build(); +``` + +## FAQ + +### Why don't you use plain `enum` classes? + +Java `enum` classes are not trivially [forwards compatible](https://www.stainless.com/blog/making-java-enums-forwards-compatible). Using them in the SDK could cause runtime exceptions if the API is updated to respond with a new enum value. + +### Why do you represent fields using `JsonField` instead of just plain `T`? + +Using `JsonField` enables a few features: + +- Allowing usage of [undocumented API functionality](#undocumented-api-functionality) +- Lazily [validating the API response against the expected shape](#response-validation) +- Representing absent vs explicitly null values + +### Why don't you use [`data` classes](https://kotlinlang.org/docs/data-classes.html)? + +It is not [backwards compatible to add new fields to a data class](https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api) and we don't want to introduce a breaking change every time we add a field to a class. + +### Why don't you use checked exceptions? + +Checked exceptions are widely considered a mistake in the Java programming language. In fact, they were omitted from Kotlin for this reason. + +Checked exceptions: + +- Are verbose to handle +- Encourage error handling at the wrong level of abstraction, where nothing can be done about the error +- Are tedious to propagate due to the [function coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function) +- Don't play well with lambdas (also due to the function coloring problem) + +## Semantic versioning + +This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: + +1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ +2. Changes that we do not expect to impact the vast majority of users in practice. + +We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. + +We are keen for your feedback; please open an [issue](https://www.github.com/browserbase/stagehand-java/issues) with questions, bugs, or suggestions. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..dcfc419 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainless.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Stagehand, please follow the respective company's security reporting guidelines. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..3a6a7b4 --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${SONATYPE_USERNAME}" ]; then + errors+=("The SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +if [ -z "${SONATYPE_PASSWORD}" ]; then + errors+=("The SONATYPE_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +if [ -z "${GPG_SIGNING_KEY}" ]; then + errors+=("The GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +if [ -z "${GPG_SIGNING_PASSWORD}" ]; then + errors+=("The GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/browserbase-java-example/build.gradle.kts b/browserbase-java-example/build.gradle.kts new file mode 100644 index 0000000..61df3c5 --- /dev/null +++ b/browserbase-java-example/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("browserbase.java") + application +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":browserbase-java")) +} + +tasks.withType().configureEach { + // Allow using more modern APIs, like `List.of` and `Map.of`, in examples. + options.release.set(9) +} + +application { + // Use `./gradlew :browserbase-java-example:run` to run `Main` + // Use `./gradlew :browserbase-java-example:run -Pexample=Something` to run `SomethingExample` + mainClass = "com.stagehand.api.example.${ + if (project.hasProperty("example")) + "${project.property("example")}Example" + else + "Main" + }" +} diff --git a/browserbase-java-lib/.keep b/browserbase-java-lib/.keep new file mode 100644 index 0000000..5e2c99f --- /dev/null +++ b/browserbase-java-lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..4602d16 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("org.jetbrains.dokka") version "2.0.0" +} + +repositories { + mavenCentral() +} + +allprojects { + group = "com.browserbase.api" + version = "3.20.0" // x-release-please-version +} + +subprojects { + // These are populated with dependencies by `buildSrc` scripts. + tasks.register("format") { + group = "Verification" + description = "Formats all source files." + } + tasks.register("lint") { + group = "Verification" + description = "Verifies all source files are formatted." + } +} + +subprojects { + apply(plugin = "org.jetbrains.dokka") +} + +// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks +tasks.named("dokkaJavadocCollector").configure { + subprojects.flatMap { it.tasks } + .filter { it.project.name != "stagehand-java" && it.name == "dokkaJavadocJar" } + .forEach { mustRunAfter(it) } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..c6dc92e --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `kotlin-dsl` + kotlin("jvm") version "1.9.20" + id("com.vanniktech.maven.publish") version "0.28.0" +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") + implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0") +} diff --git a/buildSrc/src/main/kotlin/stagehand.java.gradle.kts b/buildSrc/src/main/kotlin/stagehand.java.gradle.kts new file mode 100644 index 0000000..8f4f902 --- /dev/null +++ b/buildSrc/src/main/kotlin/stagehand.java.gradle.kts @@ -0,0 +1,127 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + +plugins { + `java-library` +} + +repositories { + mavenCentral() +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + options.compilerArgs.add("-Werror") + options.release.set(8) +} + +tasks.named("jar") { + manifest { + attributes(mapOf( + "Implementation-Title" to project.name, + "Implementation-Version" to project.version + )) + } +} + +tasks.withType().configureEach { + useJUnitPlatform() + + // Run tests in parallel to some degree. + maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) + forkEvery = 100 + + testLogging { + exceptionFormat = TestExceptionFormat.FULL + } +} + +val palantir by configurations.creating +dependencies { + palantir("com.palantir.javaformat:palantir-java-format:2.89.0") +} + +fun registerPalantir( + name: String, + description: String, +) { + val javaName = "${name}Java" + tasks.register(javaName) { + group = "Verification" + this.description = description + + classpath = palantir + mainClass = "com.palantir.javaformat.java.Main" + + // Avoid an `IllegalAccessError` on Java 9+. + jvmArgs( + "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + ) + + // Use paths relative to the current module. + val argumentFile = + project.layout.buildDirectory.file("palantir-$name-args.txt").get().asFile + val lastRunTimeFile = + project.layout.buildDirectory.file("palantir-$name-last-run.txt").get().asFile + + // Read the time when this task was last executed for this module (if ever). + val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L + + // Use a `fileTree` relative to the module's source directory. + val javaFiles = project.fileTree("src") { include("**/*.java") } + + // Determine if any files need to be formatted or linted and continue only if there is at least + // one file. + onlyIf { javaFiles.any { it.lastModified() > lastRunTime } } + + inputs.files(javaFiles) + + doFirst { + // Create the argument file and set the preferred formatting style. + argumentFile.parentFile.mkdirs() + argumentFile.writeText("--palantir\n") + + if (name == "lint") { + // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of + // the default 0) if any files need to be formatted, indicating that linting has failed. + argumentFile.appendText("--dry-run\n") + argumentFile.appendText("--set-exit-if-changed\n") + } else { + // `--dry-run` and `--replace` (for in-place formatting) are mutually exclusive. + argumentFile.appendText("--replace\n") + } + + // Write the modified files to the argument file. + javaFiles.filter { it.lastModified() > lastRunTime } + .forEach { argumentFile.appendText("${it.absolutePath}\n") } + } + + doLast { + // Record the last execution time for later up-to-date checking. + lastRunTimeFile.writeText(System.currentTimeMillis().toString()) + } + + // Pass the argument file using the @ symbol + args = listOf("@${argumentFile.absolutePath}") + + outputs.upToDateWhen { javaFiles.none { it.lastModified() > lastRunTime } } + } + + tasks.named(name) { + dependsOn(tasks.named(javaName)) + } +} + +registerPalantir(name = "format", description = "Formats all Java source files.") +registerPalantir(name = "lint", description = "Verifies all Java source files are formatted.") diff --git a/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts b/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts new file mode 100644 index 0000000..2e59660 --- /dev/null +++ b/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts @@ -0,0 +1,109 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + +plugins { + id("stagehand.java") + kotlin("jvm") +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + + compilerOptions { + freeCompilerArgs = listOf( + "-Xjvm-default=all", + "-Xjdk-release=1.8", + // 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_1_8) + languageVersion.set(KotlinVersion.KOTLIN_1_8) + apiVersion.set(KotlinVersion.KOTLIN_1_8) + coreLibrariesVersion = "1.8.0" + } +} + +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 +dependencies { + ktfmt("com.facebook:ktfmt:0.61") +} + +fun registerKtfmt( + name: String, + description: String, +) { + val kotlinName = "${name}Kotlin" + tasks.register(kotlinName) { + group = "Verification" + this.description = description + + classpath = ktfmt + mainClass = "com.facebook.ktfmt.cli.Main" + + // Use paths relative to the current module. + val argumentFile = project.layout.buildDirectory.file("ktfmt-$name-args.txt").get().asFile + val lastRunTimeFile = + project.layout.buildDirectory.file("ktfmt-$name-last-run.txt").get().asFile + + // Read the time when this task was last executed for this module (if ever). + val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L + + // Use a `fileTree` relative to the module's source directory. + val kotlinFiles = project.fileTree("src") { include("**/*.kt") } + + // Determine if any files need to be formatted or linted and continue only if there is at least + // one file (otherwise Ktfmt will fail). + onlyIf { kotlinFiles.any { it.lastModified() > lastRunTime } } + + inputs.files(kotlinFiles) + + doFirst { + // Create the argument file and set the preferred formatting style. + argumentFile.parentFile.mkdirs() + argumentFile.writeText("--kotlinlang-style\n") + + if (name == "lint") { + // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of + // the default 0) if any files need to be formatted, indicating that linting has failed. + argumentFile.appendText("--dry-run\n") + argumentFile.appendText("--set-exit-if-changed\n") + } + + // Write the modified files to the argument file. + kotlinFiles.filter { it.lastModified() > lastRunTime } + .forEach { argumentFile.appendText("${it.absolutePath}\n") } + } + + doLast { + // Record the last execution time for later up-to-date checking. + lastRunTimeFile.writeText(System.currentTimeMillis().toString()) + } + + // Pass the argument file using the @ symbol + args = listOf("@${argumentFile.absolutePath}") + + outputs.upToDateWhen { kotlinFiles.none { it.lastModified() > lastRunTime } } + } + + tasks.named(name) { + dependsOn(tasks.named(kotlinName)) + } +} + +registerKtfmt(name = "format", description = "Formats all Kotlin source files.") +registerKtfmt(name = "lint", description = "Verifies all Kotlin source files are formatted.") diff --git a/buildSrc/src/main/kotlin/stagehand.publish.gradle.kts b/buildSrc/src/main/kotlin/stagehand.publish.gradle.kts new file mode 100644 index 0000000..50ca9dc --- /dev/null +++ b/buildSrc/src/main/kotlin/stagehand.publish.gradle.kts @@ -0,0 +1,71 @@ +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinJvm +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.SonatypeHost + +plugins { + id("com.vanniktech.maven.publish") +} + +publishing { + repositories { + if (project.hasProperty("publishLocal")) { + maven { + name = "LocalFileSystem" + url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo") + } + } + } +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +extra["signingInMemoryKey"] = System.getenv("GPG_SIGNING_KEY") +extra["signingInMemoryKeyId"] = System.getenv("GPG_SIGNING_KEY_ID") +extra["signingInMemoryKeyPassword"] = System.getenv("GPG_SIGNING_PASSWORD") + +configure { + if (!project.hasProperty("publishLocal")) { + signAllPublications() + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + } + + coordinates(project.group.toString(), project.name, project.version.toString()) + configure( + KotlinJvm( + javadocJar = JavadocJar.Dokka("dokkaJavadoc"), + sourcesJar = true, + ) + ) + + pom { + name.set("Stagehand API") + description.set("Stagehand SDK for AI browser automation [ALPHA]. This API allows clients to\nexecute browser automation tasks remotely on the Browserbase cloud. All\nendpoints except /sessions/start require an active session ID. Responses are\nstreamed using Server-Sent Events (SSE) when the `x-stream-response: true`\nheader is provided.\n\nThis SDK is currently ALPHA software and is not production ready! Please try it\nand give us your feedback, stay tuned for upcoming release announcements!") + url.set("https://docs.stagehand.dev") + + licenses { + license { + name.set("MIT") + } + } + + developers { + developer { + name.set("Stagehand") + } + } + + scm { + connection.set("scm:git:git://github.com/browserbase/stagehand-java.git") + developerConnection.set("scm:git:git://github.com/browserbase/stagehand-java.git") + url.set("https://github.com/browserbase/stagehand-java") + } + } +} + +tasks.withType().configureEach { + isZip64 = true +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6680f9c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +org.gradle.caching=true +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.daemon=false +# These options improve our compilation and test performance. They are inherited by the Kotlin daemon. +org.gradle.jvmargs=\ + -Xms2g \ + -Xmx8g \ + -XX:+UseParallelGC \ + -XX:InitialCodeCacheSize=256m \ + -XX:ReservedCodeCacheSize=1G \ + -XX:MetaspaceSize=512m \ + -XX:MaxMetaspaceSize=2G \ + -XX:TieredStopAtLevel=1 \ + -XX:GCTimeRatio=4 \ + -XX:CICompilerCount=4 \ + -XX:+OptimizeStringConcat \ + -XX:+UseStringDeduplication diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cea7a79 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f3b75f3 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..8f98719 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,67 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "simple", + "extra-files": [ + "README.md", + "build.gradle.kts" + ] +} \ No newline at end of file diff --git a/scripts/build b/scripts/build new file mode 100755 index 0000000..16a2b00 --- /dev/null +++ b/scripts/build @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Building classes" +./gradlew build testClasses "$@" -x test diff --git a/scripts/fast-format b/scripts/fast-format new file mode 100755 index 0000000..35a1dee --- /dev/null +++ b/scripts/fast-format @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo "Script started with $# arguments" +echo "Arguments: $*" +echo "Script location: $(dirname "$0")" + +cd "$(dirname "$0")/.." +echo "Changed to directory: $(pwd)" + +if [ $# -eq 0 ]; then + echo "Usage: $0 [additional-formatter-args...]" + echo "The file should contain one file path per line" + exit 1 +fi + +FILE_LIST="$1" + +echo "Looking for file: $FILE_LIST" + +if [ ! -f "$FILE_LIST" ]; then + echo "Error: File '$FILE_LIST' not found" + exit 1 +fi + +if ! command -v ktfmt &> /dev/null; then + echo "Error: ktfmt not found" + exit 1 +fi + +# Process Kotlin files +echo "==> Looking for Kotlin files" +kt_files=$(grep -E '\.kt$' "$FILE_LIST" | grep -v './buildSrc/build/' || true) +echo "==> Done looking for Kotlin files" + +if [[ -n "$kt_files" ]]; then + echo "==> will format Kotlin files" + echo "$kt_files" | tr '\n' '\0' | xargs -0 ktfmt --kotlinlang-style "$@" +else + echo "No Kotlin files to format -- expected outcome during incremental formatting" +fi + +# TODO(mbudayr): support palantir-java-format +# Process Java files +# grep -E '\.java$' "$FILE_LIST" | grep -v './buildSrc/build/' | tr '\n' '\0' | xargs -0 -r palantir-java-format --palantir --replace "$@" diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..65db176 --- /dev/null +++ b/scripts/format @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if command -v ktfmt &> /dev/null; then + echo "==> Running ktfmt" + ./scripts/kotlin-format +else + echo "==> Running gradlew formatKotlin" + ./gradlew formatKotlin +fi + +if command -v palantir-java-format &> /dev/null; then + echo "==> Running palantir-java-format" + ./scripts/java-format +else + echo "==> Running gradlew formatJava" + ./gradlew formatJava +fi diff --git a/scripts/java-format b/scripts/java-format new file mode 100755 index 0000000..ad5febc --- /dev/null +++ b/scripts/java-format @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +find . -name "*.java" -not -path "./buildSrc/build/*" -print0 | xargs -0 -r palantir-java-format --palantir --replace "$@" diff --git a/scripts/kotlin-format b/scripts/kotlin-format new file mode 100755 index 0000000..3b8be9e --- /dev/null +++ b/scripts/kotlin-format @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +find . -name "*.kt" -not -path "./buildSrc/build/*" -print0 | xargs -0 -r ktfmt --kotlinlang-style "$@" diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..dbc8f77 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running lints" + +if command -v ktfmt &> /dev/null; then + echo "==> Checking ktfmt" + ./scripts/kotlin-format --dry-run --set-exit-if-changed +else + echo "==> Running gradlew lintKotlin" + ./gradlew lintKotlin +fi + +if command -v palantir-java-format &> /dev/null; then + echo "==> Checking palantir-java-format" + ./scripts/java-format --dry-run --set-exit-if-changed +else + echo "==> Running gradlew lintJava" + ./gradlew lintJava +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..904aea6 --- /dev/null +++ b/scripts/test @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + + + +echo "==> Running tests" +./gradlew test "$@" diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts new file mode 100755 index 0000000..10f3c70 --- /dev/null +++ b/scripts/upload-artifacts @@ -0,0 +1,193 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# ANSI Color Codes +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" + 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#"${MAVEN_REPO_PATH}"}" + + # 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" ;; + html) content_type="text/html" ;; + *) content_type="application/octet-stream" ;; + esac + + # 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 to 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 +} + +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.

+ +

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>
+        <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+    </repository>
+</repositories>
+ +

Gradle with authentication

+

Add the following to your build.gradle file:

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

Using the repository

+

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

+ + +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" +./gradlew publishMavenPublicationToLocalFileSystemRepository -PpublishLocal +echo "::endgroup::" + +echo "::group::Uploading to pkg.stainless.com" +walk_tree "$MAVEN_REPO_PATH" +echo "::endgroup::" + +echo "::group::Generating instructions" +generate_instructions +echo "::endgroup::" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..02623dd --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,14 @@ +rootProject.name = "stagehand-java-root" + +val projectNames = rootDir.listFiles() + ?.asSequence() + .orEmpty() + .filter { file -> + file.isDirectory && + file.name.startsWith("stagehand-java") && + file.listFiles()?.asSequence().orEmpty().any { it.name == "build.gradle.kts" } + } + .map { it.name } + .toList() +println("projects: $projectNames") +projectNames.forEach { include(it) } diff --git a/stagehand-java-client-okhttp/build.gradle.kts b/stagehand-java-client-okhttp/build.gradle.kts new file mode 100644 index 0000000..be765aa --- /dev/null +++ b/stagehand-java-client-okhttp/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("stagehand.kotlin") + id("stagehand.publish") +} + +dependencies { + api(project(":stagehand-java-core")) + + implementation("com.squareup.okhttp3:okhttp:4.12.0") + + testImplementation(kotlin("test")) + testImplementation("org.assertj:assertj-core:3.27.7") + testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2") +} diff --git a/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt b/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt new file mode 100644 index 0000000..155ae2b --- /dev/null +++ b/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt @@ -0,0 +1,356 @@ +package com.browserbase.api.client.okhttp + +import com.browserbase.api.core.RequestOptions +import com.browserbase.api.core.Timeout +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.HttpClient +import com.browserbase.api.core.http.HttpMethod +import com.browserbase.api.core.http.HttpRequest +import com.browserbase.api.core.http.HttpRequestBody +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.core.http.ProxyAuthenticator +import com.browserbase.api.errors.StagehandIoException +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.Proxy +import java.time.Duration +import java.util.concurrent.CancellationException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorService +import java.util.concurrent.TimeUnit +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager +import kotlin.jvm.optionals.getOrNull +import okhttp3.Call +import okhttp3.Callback +import okhttp3.ConnectionPool +import okhttp3.Dispatcher +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okio.BufferedSink +import okio.buffer +import okio.sink + +class OkHttpClient +internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient { + + override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse { + val call = newCall(request, requestOptions) + + return try { + call.execute().toHttpResponse() + } catch (e: IOException) { + throw StagehandIoException("Request failed", e) + } finally { + request.body?.close() + } + } + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture { + val future = CompletableFuture() + + val call = newCall(request, requestOptions) + call.enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + future.complete(response.toHttpResponse()) + } + + override fun onFailure(call: Call, e: IOException) { + future.completeExceptionally(StagehandIoException("Request failed", e)) + } + } + ) + + future.whenComplete { _, e -> + if (e is CancellationException) { + call.cancel() + } + request.body?.close() + } + + return future + } + + override fun close() { + okHttpClient.dispatcher.executorService.shutdown() + okHttpClient.connectionPool.evictAll() + okHttpClient.cache?.close() + } + + private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call { + val clientBuilder = okHttpClient.newBuilder() + + requestOptions.timeout?.let { + clientBuilder + .connectTimeout(it.connect()) + .readTimeout(it.read()) + .writeTimeout(it.write()) + .callTimeout(it.request()) + } + + val client = clientBuilder.build() + return client.newCall(request.toRequest(client)) + } + + companion object { + @JvmStatic fun builder() = Builder() + } + + class Builder internal constructor() { + + private var timeout: Timeout = Timeout.default() + private var proxy: Proxy? = null + private var proxyAuthenticator: ProxyAuthenticator? = null + private var maxIdleConnections: Int? = null + private var keepAliveDuration: Duration? = null + private var dispatcherExecutorService: ExecutorService? = null + private var sslSocketFactory: SSLSocketFactory? = null + private var trustManager: X509TrustManager? = null + private var hostnameVerifier: HostnameVerifier? = null + + fun timeout(timeout: Timeout) = apply { this.timeout = timeout } + + fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build()) + + fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } + + fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply { + this.proxyAuthenticator = proxyAuthenticator + } + + /** + * Sets the maximum number of idle connections kept by the underlying [ConnectionPool]. + * + * If this is set, then [keepAliveDuration] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun maxIdleConnections(maxIdleConnections: Int?) = apply { + this.maxIdleConnections = maxIdleConnections + } + + /** + * Sets the keep-alive duration for idle connections in the underlying [ConnectionPool]. + * + * If this is set, then [maxIdleConnections] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun keepAliveDuration(keepAliveDuration: Duration?) = apply { + this.keepAliveDuration = keepAliveDuration + } + + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { + this.sslSocketFactory = sslSocketFactory + } + + fun trustManager(trustManager: X509TrustManager?) = apply { + this.trustManager = trustManager + } + + fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply { + this.hostnameVerifier = hostnameVerifier + } + + 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()) + .callTimeout(timeout.request()) + .proxy(proxy) + .apply { + proxyAuthenticator?.let { auth -> + proxyAuthenticator { route, response -> + auth + .authenticate( + route?.proxy ?: Proxy.NO_PROXY, + response.request.toHttpRequest(), + response.toHttpResponse(), + ) + .getOrNull() + ?.toRequest(client = null) + } + } + + dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) } + + val maxIdleConnections = maxIdleConnections + val keepAliveDuration = keepAliveDuration + if (maxIdleConnections != null && keepAliveDuration != null) { + connectionPool( + ConnectionPool( + maxIdleConnections, + keepAliveDuration.toNanos(), + TimeUnit.NANOSECONDS, + ) + ) + } else { + check((maxIdleConnections != null) == (keepAliveDuration != null)) { + "Both or none of `maxIdleConnections` and `keepAliveDuration` must be set, but only one was set" + } + } + + val sslSocketFactory = sslSocketFactory + val trustManager = trustManager + if (sslSocketFactory != null && trustManager != null) { + sslSocketFactory(sslSocketFactory, trustManager) + } else { + check((sslSocketFactory != null) == (trustManager != null)) { + "Both or none of `sslSocketFactory` and `trustManager` must be set, but only one was set" + } + } + + hostnameVerifier?.let(::hostnameVerifier) + } + .build() + .apply { + // We usually make all our requests to the same host so it makes sense to + // raise the per-host limit to the overall limit. + dispatcher.maxRequestsPerHost = dispatcher.maxRequests + } + ) + } +} + +private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient?): Request { + var body: RequestBody? = body?.toRequestBody() + if (body == null && requiresBody(method)) { + body = "".toRequestBody() + } + + val builder = Request.Builder().url(toUrl()).method(method.name, body) + headers.names().forEach { name -> headers.values(name).forEach { builder.addHeader(name, it) } } + + if (client != null) { + if ( + !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0 + ) { + builder.addHeader( + "X-Stainless-Read-Timeout", + Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(), + ) + } + if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) { + builder.addHeader( + "X-Stainless-Timeout", + Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(), + ) + } + } + + return builder.build() +} + +/** `OkHttpClient` always requires a request body for some methods. */ +private fun requiresBody(method: HttpMethod): Boolean = + when (method) { + HttpMethod.POST, + HttpMethod.PUT, + HttpMethod.PATCH -> true + else -> false + } + +private fun HttpRequest.toUrl(): String { + val builder = baseUrl.toHttpUrl().newBuilder() + pathSegments.forEach(builder::addPathSegment) + queryParams.keys().forEach { key -> + queryParams.values(key).forEach { builder.addQueryParameter(key, it) } + } + + return builder.toString() +} + +private fun HttpRequestBody.toRequestBody(): RequestBody { + val mediaType = contentType()?.toMediaType() + val length = contentLength() + + return object : RequestBody() { + override fun contentType(): MediaType? = mediaType + + override fun contentLength(): Long = length + + override fun isOneShot(): Boolean = !repeatable() + + override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream()) + } +} + +private fun Request.toHttpRequest(): HttpRequest { + val builder = HttpRequest.builder().method(HttpMethod.valueOf(method)).baseUrl(url.toBaseUrl()) + url.pathSegments.forEach(builder::addPathSegment) + url.queryParameterNames.forEach { name -> + url.queryParameterValues(name).filterNotNull().forEach { builder.putQueryParam(name, it) } + } + headers.forEach { (name, value) -> builder.putHeader(name, value) } + body?.let { builder.body(it.toHttpRequestBody()) } + return builder.build() +} + +private fun HttpUrl.toBaseUrl(): String = buildString { + append(scheme).append("://").append(host) + if (port != HttpUrl.defaultPort(scheme)) { + append(":").append(port) + } +} + +private fun RequestBody.toHttpRequestBody(): HttpRequestBody { + val mediaType = contentType()?.toString() + val length = contentLength() + val isOneShot = isOneShot() + val source = this + return object : HttpRequestBody { + override fun contentType(): String? = mediaType + + override fun contentLength(): Long = length + + override fun repeatable(): Boolean = !isOneShot + + override fun writeTo(outputStream: OutputStream) { + val sink = outputStream.sink().buffer() + source.writeTo(sink) + sink.flush() + } + + override fun close() {} + } +} + +private fun Response.toHttpResponse(): HttpResponse { + val headers = headers.toHeaders() + + return object : HttpResponse { + override fun statusCode(): Int = code + + override fun headers(): Headers = headers + + override fun body(): InputStream = body!!.byteStream() + + override fun close() = body!!.close() + } +} + +private fun okhttp3.Headers.toHeaders(): Headers { + val headersBuilder = Headers.builder() + forEach { (name, value) -> headersBuilder.put(name, value) } + return headersBuilder.build() +} diff --git a/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt b/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt new file mode 100644 index 0000000..36e0fe3 --- /dev/null +++ b/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt @@ -0,0 +1,437 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.client.okhttp + +import com.browserbase.api.client.StagehandClient +import com.browserbase.api.client.StagehandClientImpl +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.core.LogLevel +import com.browserbase.api.core.Sleeper +import com.browserbase.api.core.Timeout +import com.browserbase.api.core.http.AsyncStreamResponse +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.HttpClient +import com.browserbase.api.core.http.ProxyAuthenticator +import com.browserbase.api.core.http.QueryParams +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.databind.json.JsonMapper +import java.net.Proxy +import java.time.Clock +import java.time.Duration +import java.util.Optional +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager +import kotlin.jvm.optionals.getOrNull + +/** + * A class that allows building an instance of [StagehandClient] with [OkHttpClient] as the + * underlying [HttpClient]. + */ +class StagehandOkHttpClient private constructor() { + + companion object { + + /** Returns a mutable builder for constructing an instance of [StagehandClient]. */ + @JvmStatic fun builder() = Builder() + + /** + * Returns a client configured using system properties and environment variables. + * + * @see ClientOptions.Builder.fromEnv + */ + @JvmStatic fun fromEnv(): StagehandClient = builder().fromEnv().build() + } + + /** A builder for [StagehandOkHttpClient]. */ + class Builder internal constructor() { + + private var clientOptions: ClientOptions.Builder = ClientOptions.builder() + private var dispatcherExecutorService: ExecutorService? = null + private var proxy: Proxy? = null + private var proxyAuthenticator: ProxyAuthenticator? = null + private var maxIdleConnections: Int? = null + private var keepAliveDuration: Duration? = 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)`. */ + fun proxy(proxy: Optional) = proxy(proxy.getOrNull()) + + /** + * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication + * Required`. + */ + fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply { + this.proxyAuthenticator = proxyAuthenticator + } + + /** + * Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`. + */ + fun proxyAuthenticator(proxyAuthenticator: Optional) = + proxyAuthenticator(proxyAuthenticator.getOrNull()) + + /** + * The maximum number of idle connections kept by the underlying OkHttp connection pool. + * + * If this is set, then [keepAliveDuration] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun maxIdleConnections(maxIdleConnections: Int?) = apply { + this.maxIdleConnections = maxIdleConnections + } + + /** + * Alias for [Builder.maxIdleConnections]. + * + * This unboxed primitive overload exists for backwards compatibility. + */ + fun maxIdleConnections(maxIdleConnections: Int) = + maxIdleConnections(maxIdleConnections as Int?) + + /** + * Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`. + */ + fun maxIdleConnections(maxIdleConnections: Optional) = + maxIdleConnections(maxIdleConnections.getOrNull()) + + /** + * The keep-alive duration for idle connections in the underlying OkHttp connection pool. + * + * If this is set, then [maxIdleConnections] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun keepAliveDuration(keepAliveDuration: Duration?) = apply { + this.keepAliveDuration = keepAliveDuration + } + + /** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */ + fun keepAliveDuration(keepAliveDuration: Optional) = + keepAliveDuration(keepAliveDuration.getOrNull()) + + /** + * The socket factory used to secure HTTPS connections. + * + * If this is set, then [trustManager] must also be set. + * + * If unset, then the system default is used. Most applications should not call this method, + * and instead use the system default. The default include special optimizations that can be + * lost if the implementation is modified. + */ + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { + this.sslSocketFactory = sslSocketFactory + } + + /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */ + fun sslSocketFactory(sslSocketFactory: Optional) = + sslSocketFactory(sslSocketFactory.getOrNull()) + + /** + * The trust manager used to secure HTTPS connections. + * + * If this is set, then [sslSocketFactory] must also be set. + * + * If unset, then the system default is used. Most applications should not call this method, + * and instead use the system default. The default include special optimizations that can be + * lost if the implementation is modified. + */ + fun trustManager(trustManager: X509TrustManager?) = apply { + this.trustManager = trustManager + } + + /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */ + fun trustManager(trustManager: Optional) = + trustManager(trustManager.getOrNull()) + + /** + * The verifier used to confirm that response certificates apply to requested hostnames for + * HTTPS connections. + * + * If unset, then a default hostname verifier is used. + */ + fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply { + this.hostnameVerifier = hostnameVerifier + } + + /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */ + fun hostnameVerifier(hostnameVerifier: Optional) = + hostnameVerifier(hostnameVerifier.getOrNull()) + + /** + * Whether to throw an exception if any of the Jackson versions detected at runtime are + * incompatible with the SDK's minimum supported Jackson version (2.13.4). + * + * Defaults to true. Use extreme caution when disabling this option. There is no guarantee + * that the SDK will work correctly when using an incompatible Jackson version. + */ + fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply { + clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility) + } + + /** + * The Jackson JSON mapper to use for serializing and deserializing JSON. + * + * Defaults to [com.browserbase.api.core.jsonMapper]. The default is usually sufficient and + * rarely needs to be overridden. + */ + fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) } + + /** + * The executor to use for running [AsyncStreamResponse.Handler] callbacks. + * + * Defaults to a dedicated cached thread pool. + * + * This class takes ownership of the executor and shuts it down, if possible, when closed. + */ + fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply { + clientOptions.streamHandlerExecutor(streamHandlerExecutor) + } + + /** + * The interface to use for delaying execution, like during retries. + * + * This is primarily useful for using fake delays in tests. + * + * Defaults to real execution delays. + * + * This class takes ownership of the sleeper and closes it when closed. + */ + fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) } + + /** + * The clock to use for operations that require timing, like retries. + * + * This is primarily useful for using a fake clock in tests. + * + * Defaults to [Clock.systemUTC]. + */ + fun clock(clock: Clock) = apply { clientOptions.clock(clock) } + + /** + * The base URL to use for every request. + * + * Defaults to the production environment: `https://api.stagehand.browserbase.com`. + */ + fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) } + + /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */ + fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull()) + + /** + * Whether to call `validate` on every response before returning it. + * + * Setting this to `true` is _not_ forwards compatible with new types from the API for + * existing fields. + * + * Defaults to false, which means the shape of the response will not be validated upfront. + * Instead, validation will only occur for the parts of the response that are accessed. + */ + fun responseValidation(responseValidation: Boolean) = apply { + clientOptions.responseValidation(responseValidation) + } + + /** + * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding + * retries. + * + * Defaults to [Timeout.default]. + */ + fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) } + + /** + * Sets the maximum time allowed for a complete HTTP call, not including retries. + * + * See [Timeout.request] for more details. + * + * For fine-grained control, pass a [Timeout] object. + */ + fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) } + + /** + * The maximum number of times to retry failed requests, with a short exponential backoff + * between requests. + * + * Only the following error types are retried: + * - Connection errors (for example, due to a network connectivity problem) + * - 408 Request Timeout + * - 409 Conflict + * - 429 Rate Limit + * - 5xx Internal + * + * The API may also explicitly instruct the SDK to retry or not retry a request. + * + * Defaults to 2. + */ + fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) } + + /** + * The level at which to log request and response information. + * + * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv]. + * + * Defaults to [LogLevel.fromEnv]. + */ + fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) } + + /** Your [Browserbase API Key](https://www.browserbase.com/settings) */ + fun browserbaseApiKey(browserbaseApiKey: String) = apply { + clientOptions.browserbaseApiKey(browserbaseApiKey) + } + + /** + * Deprecated. Browserbase API keys are now project-scoped, so this value is no longer + * required. + */ + fun browserbaseProjectId(browserbaseProjectId: String?) = apply { + clientOptions.browserbaseProjectId(browserbaseProjectId) + } + + /** + * Alias for calling [Builder.browserbaseProjectId] with + * `browserbaseProjectId.orElse(null)`. + */ + fun browserbaseProjectId(browserbaseProjectId: Optional) = + browserbaseProjectId(browserbaseProjectId.getOrNull()) + + /** Your LLM provider API key (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) */ + fun modelApiKey(modelApiKey: String) = apply { clientOptions.modelApiKey(modelApiKey) } + + fun headers(headers: Headers) = apply { clientOptions.headers(headers) } + + fun headers(headers: Map>) = apply { + clientOptions.headers(headers) + } + + fun putHeader(name: String, value: String) = apply { clientOptions.putHeader(name, value) } + + fun putHeaders(name: String, values: Iterable) = apply { + clientOptions.putHeaders(name, values) + } + + fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) } + + fun putAllHeaders(headers: Map>) = apply { + clientOptions.putAllHeaders(headers) + } + + fun replaceHeaders(name: String, value: String) = apply { + clientOptions.replaceHeaders(name, value) + } + + fun replaceHeaders(name: String, values: Iterable) = apply { + clientOptions.replaceHeaders(name, values) + } + + fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) } + + fun replaceAllHeaders(headers: Map>) = apply { + clientOptions.replaceAllHeaders(headers) + } + + fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) } + + fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) } + + fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) } + + fun queryParams(queryParams: Map>) = apply { + clientOptions.queryParams(queryParams) + } + + fun putQueryParam(key: String, value: String) = apply { + clientOptions.putQueryParam(key, value) + } + + fun putQueryParams(key: String, values: Iterable) = apply { + clientOptions.putQueryParams(key, values) + } + + fun putAllQueryParams(queryParams: QueryParams) = apply { + clientOptions.putAllQueryParams(queryParams) + } + + fun putAllQueryParams(queryParams: Map>) = apply { + clientOptions.putAllQueryParams(queryParams) + } + + fun replaceQueryParams(key: String, value: String) = apply { + clientOptions.replaceQueryParams(key, value) + } + + fun replaceQueryParams(key: String, values: Iterable) = apply { + clientOptions.replaceQueryParams(key, values) + } + + fun replaceAllQueryParams(queryParams: QueryParams) = apply { + clientOptions.replaceAllQueryParams(queryParams) + } + + fun replaceAllQueryParams(queryParams: Map>) = apply { + clientOptions.replaceAllQueryParams(queryParams) + } + + fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) } + + fun removeAllQueryParams(keys: Set) = apply { + clientOptions.removeAllQueryParams(keys) + } + + /** + * Updates configuration using system properties and environment variables. + * + * @see ClientOptions.Builder.fromEnv + */ + fun fromEnv() = apply { clientOptions.fromEnv() } + + /** + * Returns an immutable instance of [StagehandClient]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): StagehandClient = + StagehandClientImpl( + clientOptions + .httpClient( + OkHttpClient.builder() + .timeout(clientOptions.timeout()) + .proxy(proxy) + .proxyAuthenticator(proxyAuthenticator) + .maxIdleConnections(maxIdleConnections) + .keepAliveDuration(keepAliveDuration) + .dispatcherExecutorService(dispatcherExecutorService) + .sslSocketFactory(sslSocketFactory) + .trustManager(trustManager) + .hostnameVerifier(hostnameVerifier) + .build() + ) + .build() + ) + } +} diff --git a/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt b/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt new file mode 100644 index 0000000..83bb57e --- /dev/null +++ b/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt @@ -0,0 +1,437 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.client.okhttp + +import com.browserbase.api.client.StagehandClientAsync +import com.browserbase.api.client.StagehandClientAsyncImpl +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.core.LogLevel +import com.browserbase.api.core.Sleeper +import com.browserbase.api.core.Timeout +import com.browserbase.api.core.http.AsyncStreamResponse +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.HttpClient +import com.browserbase.api.core.http.ProxyAuthenticator +import com.browserbase.api.core.http.QueryParams +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.databind.json.JsonMapper +import java.net.Proxy +import java.time.Clock +import java.time.Duration +import java.util.Optional +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager +import kotlin.jvm.optionals.getOrNull + +/** + * A class that allows building an instance of [StagehandClientAsync] with [OkHttpClient] as the + * underlying [HttpClient]. + */ +class StagehandOkHttpClientAsync private constructor() { + + companion object { + + /** Returns a mutable builder for constructing an instance of [StagehandClientAsync]. */ + @JvmStatic fun builder() = Builder() + + /** + * Returns a client configured using system properties and environment variables. + * + * @see ClientOptions.Builder.fromEnv + */ + @JvmStatic fun fromEnv(): StagehandClientAsync = builder().fromEnv().build() + } + + /** A builder for [StagehandOkHttpClientAsync]. */ + class Builder internal constructor() { + + private var clientOptions: ClientOptions.Builder = ClientOptions.builder() + private var dispatcherExecutorService: ExecutorService? = null + private var proxy: Proxy? = null + private var proxyAuthenticator: ProxyAuthenticator? = null + private var maxIdleConnections: Int? = null + private var keepAliveDuration: Duration? = 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)`. */ + fun proxy(proxy: Optional) = proxy(proxy.getOrNull()) + + /** + * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication + * Required`. + */ + fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply { + this.proxyAuthenticator = proxyAuthenticator + } + + /** + * Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`. + */ + fun proxyAuthenticator(proxyAuthenticator: Optional) = + proxyAuthenticator(proxyAuthenticator.getOrNull()) + + /** + * The maximum number of idle connections kept by the underlying OkHttp connection pool. + * + * If this is set, then [keepAliveDuration] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun maxIdleConnections(maxIdleConnections: Int?) = apply { + this.maxIdleConnections = maxIdleConnections + } + + /** + * Alias for [Builder.maxIdleConnections]. + * + * This unboxed primitive overload exists for backwards compatibility. + */ + fun maxIdleConnections(maxIdleConnections: Int) = + maxIdleConnections(maxIdleConnections as Int?) + + /** + * Alias for calling [Builder.maxIdleConnections] with `maxIdleConnections.orElse(null)`. + */ + fun maxIdleConnections(maxIdleConnections: Optional) = + maxIdleConnections(maxIdleConnections.getOrNull()) + + /** + * The keep-alive duration for idle connections in the underlying OkHttp connection pool. + * + * If this is set, then [maxIdleConnections] must also be set. + * + * If unset, then OkHttp's default is used. + */ + fun keepAliveDuration(keepAliveDuration: Duration?) = apply { + this.keepAliveDuration = keepAliveDuration + } + + /** Alias for calling [Builder.keepAliveDuration] with `keepAliveDuration.orElse(null)`. */ + fun keepAliveDuration(keepAliveDuration: Optional) = + keepAliveDuration(keepAliveDuration.getOrNull()) + + /** + * The socket factory used to secure HTTPS connections. + * + * If this is set, then [trustManager] must also be set. + * + * If unset, then the system default is used. Most applications should not call this method, + * and instead use the system default. The default include special optimizations that can be + * lost if the implementation is modified. + */ + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { + this.sslSocketFactory = sslSocketFactory + } + + /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */ + fun sslSocketFactory(sslSocketFactory: Optional) = + sslSocketFactory(sslSocketFactory.getOrNull()) + + /** + * The trust manager used to secure HTTPS connections. + * + * If this is set, then [sslSocketFactory] must also be set. + * + * If unset, then the system default is used. Most applications should not call this method, + * and instead use the system default. The default include special optimizations that can be + * lost if the implementation is modified. + */ + fun trustManager(trustManager: X509TrustManager?) = apply { + this.trustManager = trustManager + } + + /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */ + fun trustManager(trustManager: Optional) = + trustManager(trustManager.getOrNull()) + + /** + * The verifier used to confirm that response certificates apply to requested hostnames for + * HTTPS connections. + * + * If unset, then a default hostname verifier is used. + */ + fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply { + this.hostnameVerifier = hostnameVerifier + } + + /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */ + fun hostnameVerifier(hostnameVerifier: Optional) = + hostnameVerifier(hostnameVerifier.getOrNull()) + + /** + * Whether to throw an exception if any of the Jackson versions detected at runtime are + * incompatible with the SDK's minimum supported Jackson version (2.13.4). + * + * Defaults to true. Use extreme caution when disabling this option. There is no guarantee + * that the SDK will work correctly when using an incompatible Jackson version. + */ + fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply { + clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility) + } + + /** + * The Jackson JSON mapper to use for serializing and deserializing JSON. + * + * Defaults to [com.browserbase.api.core.jsonMapper]. The default is usually sufficient and + * rarely needs to be overridden. + */ + fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) } + + /** + * The executor to use for running [AsyncStreamResponse.Handler] callbacks. + * + * Defaults to a dedicated cached thread pool. + * + * This class takes ownership of the executor and shuts it down, if possible, when closed. + */ + fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply { + clientOptions.streamHandlerExecutor(streamHandlerExecutor) + } + + /** + * The interface to use for delaying execution, like during retries. + * + * This is primarily useful for using fake delays in tests. + * + * Defaults to real execution delays. + * + * This class takes ownership of the sleeper and closes it when closed. + */ + fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) } + + /** + * The clock to use for operations that require timing, like retries. + * + * This is primarily useful for using a fake clock in tests. + * + * Defaults to [Clock.systemUTC]. + */ + fun clock(clock: Clock) = apply { clientOptions.clock(clock) } + + /** + * The base URL to use for every request. + * + * Defaults to the production environment: `https://api.stagehand.browserbase.com`. + */ + fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) } + + /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */ + fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull()) + + /** + * Whether to call `validate` on every response before returning it. + * + * Setting this to `true` is _not_ forwards compatible with new types from the API for + * existing fields. + * + * Defaults to false, which means the shape of the response will not be validated upfront. + * Instead, validation will only occur for the parts of the response that are accessed. + */ + fun responseValidation(responseValidation: Boolean) = apply { + clientOptions.responseValidation(responseValidation) + } + + /** + * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding + * retries. + * + * Defaults to [Timeout.default]. + */ + fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) } + + /** + * Sets the maximum time allowed for a complete HTTP call, not including retries. + * + * See [Timeout.request] for more details. + * + * For fine-grained control, pass a [Timeout] object. + */ + fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) } + + /** + * The maximum number of times to retry failed requests, with a short exponential backoff + * between requests. + * + * Only the following error types are retried: + * - Connection errors (for example, due to a network connectivity problem) + * - 408 Request Timeout + * - 409 Conflict + * - 429 Rate Limit + * - 5xx Internal + * + * The API may also explicitly instruct the SDK to retry or not retry a request. + * + * Defaults to 2. + */ + fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) } + + /** + * The level at which to log request and response information. + * + * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv]. + * + * Defaults to [LogLevel.fromEnv]. + */ + fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) } + + /** Your [Browserbase API Key](https://www.browserbase.com/settings) */ + fun browserbaseApiKey(browserbaseApiKey: String) = apply { + clientOptions.browserbaseApiKey(browserbaseApiKey) + } + + /** + * Deprecated. Browserbase API keys are now project-scoped, so this value is no longer + * required. + */ + fun browserbaseProjectId(browserbaseProjectId: String?) = apply { + clientOptions.browserbaseProjectId(browserbaseProjectId) + } + + /** + * Alias for calling [Builder.browserbaseProjectId] with + * `browserbaseProjectId.orElse(null)`. + */ + fun browserbaseProjectId(browserbaseProjectId: Optional) = + browserbaseProjectId(browserbaseProjectId.getOrNull()) + + /** Your LLM provider API key (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) */ + fun modelApiKey(modelApiKey: String) = apply { clientOptions.modelApiKey(modelApiKey) } + + fun headers(headers: Headers) = apply { clientOptions.headers(headers) } + + fun headers(headers: Map>) = apply { + clientOptions.headers(headers) + } + + fun putHeader(name: String, value: String) = apply { clientOptions.putHeader(name, value) } + + fun putHeaders(name: String, values: Iterable) = apply { + clientOptions.putHeaders(name, values) + } + + fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) } + + fun putAllHeaders(headers: Map>) = apply { + clientOptions.putAllHeaders(headers) + } + + fun replaceHeaders(name: String, value: String) = apply { + clientOptions.replaceHeaders(name, value) + } + + fun replaceHeaders(name: String, values: Iterable) = apply { + clientOptions.replaceHeaders(name, values) + } + + fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) } + + fun replaceAllHeaders(headers: Map>) = apply { + clientOptions.replaceAllHeaders(headers) + } + + fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) } + + fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) } + + fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) } + + fun queryParams(queryParams: Map>) = apply { + clientOptions.queryParams(queryParams) + } + + fun putQueryParam(key: String, value: String) = apply { + clientOptions.putQueryParam(key, value) + } + + fun putQueryParams(key: String, values: Iterable) = apply { + clientOptions.putQueryParams(key, values) + } + + fun putAllQueryParams(queryParams: QueryParams) = apply { + clientOptions.putAllQueryParams(queryParams) + } + + fun putAllQueryParams(queryParams: Map>) = apply { + clientOptions.putAllQueryParams(queryParams) + } + + fun replaceQueryParams(key: String, value: String) = apply { + clientOptions.replaceQueryParams(key, value) + } + + fun replaceQueryParams(key: String, values: Iterable) = apply { + clientOptions.replaceQueryParams(key, values) + } + + fun replaceAllQueryParams(queryParams: QueryParams) = apply { + clientOptions.replaceAllQueryParams(queryParams) + } + + fun replaceAllQueryParams(queryParams: Map>) = apply { + clientOptions.replaceAllQueryParams(queryParams) + } + + fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) } + + fun removeAllQueryParams(keys: Set) = apply { + clientOptions.removeAllQueryParams(keys) + } + + /** + * Updates configuration using system properties and environment variables. + * + * @see ClientOptions.Builder.fromEnv + */ + fun fromEnv() = apply { clientOptions.fromEnv() } + + /** + * Returns an immutable instance of [StagehandClientAsync]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): StagehandClientAsync = + StagehandClientAsyncImpl( + clientOptions + .httpClient( + OkHttpClient.builder() + .timeout(clientOptions.timeout()) + .proxy(proxy) + .proxyAuthenticator(proxyAuthenticator) + .maxIdleConnections(maxIdleConnections) + .keepAliveDuration(keepAliveDuration) + .dispatcherExecutorService(dispatcherExecutorService) + .sslSocketFactory(sslSocketFactory) + .trustManager(trustManager) + .hostnameVerifier(hostnameVerifier) + .build() + ) + .build() + ) + } +} diff --git a/stagehand-java-client-okhttp/src/test/kotlin/com/browserbase/api/client/okhttp/OkHttpClientTest.kt b/stagehand-java-client-okhttp/src/test/kotlin/com/browserbase/api/client/okhttp/OkHttpClientTest.kt new file mode 100644 index 0000000..3a39e9e --- /dev/null +++ b/stagehand-java-client-okhttp/src/test/kotlin/com/browserbase/api/client/okhttp/OkHttpClientTest.kt @@ -0,0 +1,44 @@ +package com.browserbase.api.client.okhttp + +import com.browserbase.api.core.http.HttpMethod +import com.browserbase.api.core.http.HttpRequest +import com.github.tomakehurst.wiremock.client.WireMock.* +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo +import com.github.tomakehurst.wiremock.junit5.WireMockTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.ResourceLock + +@WireMockTest +@ResourceLock("https://github.com/wiremock/wiremock/issues/169") +internal class OkHttpClientTest { + + private lateinit var baseUrl: String + private lateinit var httpClient: OkHttpClient + + @BeforeEach + fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) { + baseUrl = wmRuntimeInfo.httpBaseUrl + httpClient = OkHttpClient.builder().build() + } + + @Test + fun executeAsync_whenFutureCancelled_cancelsUnderlyingCall() { + stubFor(post(urlPathEqualTo("/something")).willReturn(ok())) + val responseFuture = + httpClient.executeAsync( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build() + ) + val call = httpClient.okHttpClient.dispatcher.runningCalls().single() + + responseFuture.cancel(false) + + // Should have cancelled the underlying call + assertThat(call.isCanceled()).isTrue() + } +} diff --git a/stagehand-java-core/build.gradle.kts b/stagehand-java-core/build.gradle.kts new file mode 100644 index 0000000..55e80f4 --- /dev/null +++ b/stagehand-java-core/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("stagehand.kotlin") + id("stagehand.publish") +} + +configurations.all { + resolutionStrategy { + // 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") + } +} + +dependencies { + api("com.fasterxml.jackson.core:jackson-core:2.18.2") + api("com.fasterxml.jackson.core:jackson-databind:2.18.2") + api("com.google.errorprone:error_prone_annotations:2.33.0") + + implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2") + + testImplementation(kotlin("test")) + testImplementation(project(":stagehand-java-client-okhttp")) + testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2") + testImplementation("org.assertj:assertj-core:3.27.7") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3") + testImplementation("org.junit-pioneer:junit-pioneer:1.9.1") + testImplementation("org.mockito:mockito-core:5.14.2") + testImplementation("org.mockito:mockito-junit-jupiter:5.14.2") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClient.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClient.kt new file mode 100644 index 0000000..a5c8c6c --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClient.kt @@ -0,0 +1,72 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.client + +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.services.blocking.SessionService +import java.util.function.Consumer + +/** + * A client for interacting with the Stagehand REST API synchronously. You can also switch to + * asynchronous execution via the [async] method. + * + * This client performs best when you create a single instance and reuse it for all interactions + * with the REST API. This is because each client holds its own connection pool and thread pools. + * Reusing connections and threads reduces latency and saves memory. The client also handles rate + * limiting per client. This means that creating and using multiple instances at the same time will + * not respect rate limits. + * + * The threads and connections that are held will be released automatically if they remain idle. But + * if you are writing an application that needs to aggressively release unused resources, then you + * may call [close]. + */ +interface StagehandClient { + + /** + * Returns a version of this client that uses asynchronous execution. + * + * The returned client shares its resources, like its connection pool and thread pools, with + * this client. + */ + fun async(): StagehandClientAsync + + /** + * Returns a view of this service that provides access to raw HTTP responses for each method. + */ + fun withRawResponse(): WithRawResponse + + /** + * Returns a view of this service with the given option modifications applied. + * + * The original service is not modified. + */ + fun withOptions(modifier: Consumer): StagehandClient + + fun sessions(): SessionService + + /** + * Closes this client, relinquishing any underlying resources. + * + * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and + * usually should not be synchronously closed via try-with-resources. + * + * It's also usually not necessary to call this method at all. the default HTTP client + * automatically releases threads and connections if they remain idle, but if you are writing an + * application that needs to aggressively release unused resources, then you may call this + * method. + */ + fun close() + + /** A view of [StagehandClient] that provides access to raw HTTP responses for each method. */ + interface WithRawResponse { + + /** + * Returns a view of this service with the given option modifications applied. + * + * The original service is not modified. + */ + fun withOptions(modifier: Consumer): StagehandClient.WithRawResponse + + fun sessions(): SessionService.WithRawResponse + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsync.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsync.kt new file mode 100644 index 0000000..27fd2a9 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsync.kt @@ -0,0 +1,76 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.client + +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.services.async.SessionServiceAsync +import java.util.function.Consumer + +/** + * A client for interacting with the Stagehand REST API asynchronously. You can also switch to + * synchronous execution via the [sync] method. + * + * This client performs best when you create a single instance and reuse it for all interactions + * with the REST API. This is because each client holds its own connection pool and thread pools. + * Reusing connections and threads reduces latency and saves memory. The client also handles rate + * limiting per client. This means that creating and using multiple instances at the same time will + * not respect rate limits. + * + * The threads and connections that are held will be released automatically if they remain idle. But + * if you are writing an application that needs to aggressively release unused resources, then you + * may call [close]. + */ +interface StagehandClientAsync { + + /** + * Returns a version of this client that uses synchronous execution. + * + * The returned client shares its resources, like its connection pool and thread pools, with + * this client. + */ + fun sync(): StagehandClient + + /** + * Returns a view of this service that provides access to raw HTTP responses for each method. + */ + fun withRawResponse(): WithRawResponse + + /** + * Returns a view of this service with the given option modifications applied. + * + * The original service is not modified. + */ + fun withOptions(modifier: Consumer): StagehandClientAsync + + fun sessions(): SessionServiceAsync + + /** + * Closes this client, relinquishing any underlying resources. + * + * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and + * usually should not be synchronously closed via try-with-resources. + * + * It's also usually not necessary to call this method at all. the default HTTP client + * automatically releases threads and connections if they remain idle, but if you are writing an + * application that needs to aggressively release unused resources, then you may call this + * method. + */ + fun close() + + /** + * A view of [StagehandClientAsync] that provides access to raw HTTP responses for each method. + */ + interface WithRawResponse { + + /** + * Returns a view of this service with the given option modifications applied. + * + * The original service is not modified. + */ + fun withOptions( + modifier: Consumer + ): StagehandClientAsync.WithRawResponse + + fun sessions(): SessionServiceAsync.WithRawResponse + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt new file mode 100644 index 0000000..2a0b813 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientAsyncImpl.kt @@ -0,0 +1,59 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.client + +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.core.getPackageVersion +import com.browserbase.api.services.async.SessionServiceAsync +import com.browserbase.api.services.async.SessionServiceAsyncImpl +import java.util.function.Consumer + +class StagehandClientAsyncImpl(private val clientOptions: ClientOptions) : StagehandClientAsync { + + private val clientOptionsWithUserAgent = + if (clientOptions.headers.names().contains("User-Agent")) clientOptions + else + clientOptions + .toBuilder() + .putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}") + .build() + + // Pass the original clientOptions so that this client sets its own User-Agent. + private val sync: StagehandClient by lazy { StagehandClientImpl(clientOptions) } + + private val withRawResponse: StagehandClientAsync.WithRawResponse by lazy { + WithRawResponseImpl(clientOptions) + } + + private val sessions: SessionServiceAsync by lazy { + SessionServiceAsyncImpl(clientOptionsWithUserAgent) + } + + override fun sync(): StagehandClient = sync + + override fun withRawResponse(): StagehandClientAsync.WithRawResponse = withRawResponse + + override fun withOptions(modifier: Consumer): StagehandClientAsync = + StagehandClientAsyncImpl(clientOptions.toBuilder().apply(modifier::accept).build()) + + override fun sessions(): SessionServiceAsync = sessions + + override fun close() = clientOptions.close() + + class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) : + StagehandClientAsync.WithRawResponse { + + private val sessions: SessionServiceAsync.WithRawResponse by lazy { + SessionServiceAsyncImpl.WithRawResponseImpl(clientOptions) + } + + override fun withOptions( + modifier: Consumer + ): StagehandClientAsync.WithRawResponse = + StagehandClientAsyncImpl.WithRawResponseImpl( + clientOptions.toBuilder().apply(modifier::accept).build() + ) + + override fun sessions(): SessionServiceAsync.WithRawResponse = sessions + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt new file mode 100644 index 0000000..8ea5907 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/client/StagehandClientImpl.kt @@ -0,0 +1,57 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.client + +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.core.getPackageVersion +import com.browserbase.api.services.blocking.SessionService +import com.browserbase.api.services.blocking.SessionServiceImpl +import java.util.function.Consumer + +class StagehandClientImpl(private val clientOptions: ClientOptions) : StagehandClient { + + private val clientOptionsWithUserAgent = + if (clientOptions.headers.names().contains("User-Agent")) clientOptions + else + clientOptions + .toBuilder() + .putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}") + .build() + + // Pass the original clientOptions so that this client sets its own User-Agent. + private val async: StagehandClientAsync by lazy { StagehandClientAsyncImpl(clientOptions) } + + private val withRawResponse: StagehandClient.WithRawResponse by lazy { + WithRawResponseImpl(clientOptions) + } + + private val sessions: SessionService by lazy { SessionServiceImpl(clientOptionsWithUserAgent) } + + override fun async(): StagehandClientAsync = async + + override fun withRawResponse(): StagehandClient.WithRawResponse = withRawResponse + + override fun withOptions(modifier: Consumer): StagehandClient = + StagehandClientImpl(clientOptions.toBuilder().apply(modifier::accept).build()) + + override fun sessions(): SessionService = sessions + + override fun close() = clientOptions.close() + + class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) : + StagehandClient.WithRawResponse { + + private val sessions: SessionService.WithRawResponse by lazy { + SessionServiceImpl.WithRawResponseImpl(clientOptions) + } + + override fun withOptions( + modifier: Consumer + ): StagehandClient.WithRawResponse = + StagehandClientImpl.WithRawResponseImpl( + clientOptions.toBuilder().apply(modifier::accept).build() + ) + + override fun sessions(): SessionService.WithRawResponse = sessions + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/BaseDeserializer.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/BaseDeserializer.kt new file mode 100644 index 0000000..dc07be5 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/BaseDeserializer.kt @@ -0,0 +1,44 @@ +package com.browserbase.api.core + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.BeanProperty +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.deser.ContextualDeserializer +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import kotlin.reflect.KClass + +abstract class BaseDeserializer(type: KClass) : + StdDeserializer(type.java), ContextualDeserializer { + + override fun createContextual( + context: DeserializationContext, + property: BeanProperty?, + ): JsonDeserializer { + return this + } + + override fun deserialize(parser: JsonParser, context: DeserializationContext): T { + return parser.codec.deserialize(parser.readValueAsTree()) + } + + protected abstract fun ObjectCodec.deserialize(node: JsonNode): T + + protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: TypeReference): T? = + try { + readValue(treeAsTokens(node), type) + } catch (e: Exception) { + null + } + + protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: JavaType): T? = + try { + readValue(treeAsTokens(node), type) + } catch (e: Exception) { + null + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/BaseSerializer.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/BaseSerializer.kt new file mode 100644 index 0000000..9142bfe --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/BaseSerializer.kt @@ -0,0 +1,6 @@ +package com.browserbase.api.core + +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import kotlin.reflect.KClass + +abstract class BaseSerializer(type: KClass) : StdSerializer(type.java) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Check.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Check.kt new file mode 100644 index 0000000..0eb0c57 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Check.kt @@ -0,0 +1,96 @@ +@file:JvmName("Check") + +package com.browserbase.api.core + +import com.fasterxml.jackson.core.Version +import com.fasterxml.jackson.core.util.VersionUtil + +fun checkRequired(name: String, condition: Boolean) = + check(condition) { "`$name` is required, but was not set" } + +fun checkRequired(name: String, value: T?): T = + checkNotNull(value) { "`$name` is required, but was not set" } + +@JvmSynthetic +internal fun checkKnown(name: String, value: JsonField): T = + value.asKnown().orElseThrow { + IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}") + } + +@JvmSynthetic +internal fun checkKnown(name: String, value: MultipartField): T = + value.value.asKnown().orElseThrow { + IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}") + } + +@JvmSynthetic +internal fun checkLength(name: String, value: String, length: Int): String = + value.also { + check(it.length == length) { "`$name` must have length $length, but was ${it.length}" } + } + +@JvmSynthetic +internal fun checkMinLength(name: String, value: String, minLength: Int): String = + value.also { + check(it.length >= minLength) { + if (minLength == 1) "`$name` must be non-empty, but was empty" + else "`$name` must have at least length $minLength, but was ${it.length}" + } + } + +@JvmSynthetic +internal fun checkMaxLength(name: String, value: String, maxLength: Int): String = + value.also { + check(it.length <= maxLength) { + "`$name` must have at most length $maxLength, but was ${it.length}" + } + } + +@JvmSynthetic +internal fun checkJacksonVersionCompatibility() { + val incompatibleJacksonVersions = + RUNTIME_JACKSON_VERSIONS.mapNotNull { + val badVersionReason = BAD_JACKSON_VERSIONS[it.toString()] + when { + it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion -> + it to "incompatible major version" + it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion -> + it to "minor version too low" + it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion && + it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel -> + it to "patch version too low" + badVersionReason != null -> it to badVersionReason + else -> null + } + } + check(incompatibleJacksonVersions.isEmpty()) { + """ +This SDK requires a minimum Jackson version of $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime: + +${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) -> + "- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)" +}.joinToString("\n")} + +This can happen if you are either: +1. Directly depending on different Jackson versions +2. Depending on some library that depends on different Jackson versions, potentially transitively + +Double-check that you are depending on compatible Jackson versions. + +See https://www.github.com/browserbase/stagehand-java#jackson for more information. + """ + .trimIndent() + } +} + +private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null) +private val BAD_JACKSON_VERSIONS: Map = + mapOf("2.18.1" to "due to https://github.com/FasterXML/jackson-databind/issues/4639") +private val RUNTIME_JACKSON_VERSIONS: List = + listOf( + com.fasterxml.jackson.core.json.PackageVersion.VERSION, + com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION, + com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION, + com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION, + com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION, + ) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ClientOptions.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ClientOptions.kt new file mode 100644 index 0000000..12d3001 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ClientOptions.kt @@ -0,0 +1,601 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core + +import com.browserbase.api.core.http.AsyncStreamResponse +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.HttpClient +import com.browserbase.api.core.http.LoggingHttpClient +import com.browserbase.api.core.http.PhantomReachableClosingHttpClient +import com.browserbase.api.core.http.QueryParams +import com.browserbase.api.core.http.RetryingHttpClient +import com.fasterxml.jackson.databind.json.JsonMapper +import java.time.Clock +import java.time.Duration +import java.util.Optional +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicLong +import kotlin.jvm.optionals.getOrNull + +/** A class representing the SDK client configuration. */ +class ClientOptions +private constructor( + private val originalHttpClient: HttpClient, + /** + * The HTTP client to use in the SDK. + * + * Use the one published in `stagehand-java-client-okhttp` or implement your own. + * + * This class takes ownership of the client and closes it when closed. + */ + @get:JvmName("httpClient") val httpClient: HttpClient, + /** + * Whether to throw an exception if any of the Jackson versions detected at runtime are + * incompatible with the SDK's minimum supported Jackson version (2.13.4). + * + * Defaults to true. Use extreme caution when disabling this option. There is no guarantee that + * the SDK will work correctly when using an incompatible Jackson version. + */ + @get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean, + /** + * The Jackson JSON mapper to use for serializing and deserializing JSON. + * + * Defaults to [com.browserbase.api.core.jsonMapper]. The default is usually sufficient and + * rarely needs to be overridden. + */ + @get:JvmName("jsonMapper") val jsonMapper: JsonMapper, + /** + * The executor to use for running [AsyncStreamResponse.Handler] callbacks. + * + * Defaults to a dedicated cached thread pool. + * + * This class takes ownership of the executor and shuts it down, if possible, when closed. + */ + @get:JvmName("streamHandlerExecutor") val streamHandlerExecutor: Executor, + /** + * The interface to use for delaying execution, like during retries. + * + * This is primarily useful for using fake delays in tests. + * + * Defaults to real execution delays. + * + * This class takes ownership of the sleeper and closes it when closed. + */ + @get:JvmName("sleeper") val sleeper: Sleeper, + /** + * The clock to use for operations that require timing, like retries. + * + * This is primarily useful for using a fake clock in tests. + * + * Defaults to [Clock.systemUTC]. + */ + @get:JvmName("clock") val clock: Clock, + private val baseUrl: String?, + /** Headers to send with the request. */ + @get:JvmName("headers") val headers: Headers, + /** Query params to send with the request. */ + @get:JvmName("queryParams") val queryParams: QueryParams, + /** + * Whether to call `validate` on every response before returning it. + * + * Setting this to `true` is _not_ forwards compatible with new types from the API for existing + * fields. + * + * Defaults to false, which means the shape of the response will not be validated upfront. + * Instead, validation will only occur for the parts of the response that are accessed. + */ + @get:JvmName("responseValidation") val responseValidation: Boolean, + /** + * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding + * retries. + * + * Defaults to [Timeout.default]. + */ + @get:JvmName("timeout") val timeout: Timeout, + /** + * The maximum number of times to retry failed requests, with a short exponential backoff + * between requests. + * + * Only the following error types are retried: + * - Connection errors (for example, due to a network connectivity problem) + * - 408 Request Timeout + * - 409 Conflict + * - 429 Rate Limit + * - 5xx Internal + * + * The API may also explicitly instruct the SDK to retry or not retry a request. + * + * Defaults to 2. + */ + @get:JvmName("maxRetries") val maxRetries: Int, + /** + * The level at which to log request and response information. + * + * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv]. + * + * Defaults to [LogLevel.fromEnv]. + */ + @get:JvmName("logLevel") val logLevel: LogLevel, + /** Your [Browserbase API Key](https://www.browserbase.com/settings) */ + @get:JvmName("browserbaseApiKey") val browserbaseApiKey: String, + private val browserbaseProjectId: String?, + /** Your LLM provider API key (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) */ + @get:JvmName("modelApiKey") val modelApiKey: String, +) { + + init { + if (checkJacksonVersionCompatibility) { + checkJacksonVersionCompatibility() + } + } + + /** + * The base URL to use for every request. + * + * Defaults to the production environment: `https://api.stagehand.browserbase.com`. + */ + fun baseUrl(): String = baseUrl ?: PRODUCTION_URL + + /** + * Deprecated. Browserbase API keys are now project-scoped, so this value is no longer required. + */ + fun browserbaseProjectId(): Optional = Optional.ofNullable(browserbaseProjectId) + + fun toBuilder() = Builder().from(this) + + companion object { + + const val PRODUCTION_URL = "https://api.stagehand.browserbase.com" + + /** + * Returns a mutable builder for constructing an instance of [ClientOptions]. + * + * The following fields are required: + * ```java + * .httpClient() + * .browserbaseApiKey() + * .modelApiKey() + * ``` + */ + @JvmStatic fun builder() = Builder() + + /** + * Returns options configured using system properties and environment variables. + * + * @see Builder.fromEnv + */ + @JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build() + } + + /** A builder for [ClientOptions]. */ + class Builder internal constructor() { + + private var httpClient: HttpClient? = null + private var checkJacksonVersionCompatibility: Boolean = true + private var jsonMapper: JsonMapper = jsonMapper() + private var streamHandlerExecutor: Executor? = null + private var sleeper: Sleeper? = null + private var clock: Clock = Clock.systemUTC() + private var baseUrl: String? = null + private var headers: Headers.Builder = Headers.builder() + private var queryParams: QueryParams.Builder = QueryParams.builder() + private var responseValidation: Boolean = false + private var timeout: Timeout = Timeout.default() + private var maxRetries: Int = 2 + private var logLevel: LogLevel = LogLevel.fromEnv() + private var browserbaseApiKey: String? = null + private var browserbaseProjectId: String? = null + private var modelApiKey: String? = null + + @JvmSynthetic + internal fun from(clientOptions: ClientOptions) = apply { + httpClient = clientOptions.originalHttpClient + checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility + jsonMapper = clientOptions.jsonMapper + streamHandlerExecutor = clientOptions.streamHandlerExecutor + sleeper = clientOptions.sleeper + clock = clientOptions.clock + baseUrl = clientOptions.baseUrl + headers = clientOptions.headers.toBuilder() + queryParams = clientOptions.queryParams.toBuilder() + responseValidation = clientOptions.responseValidation + timeout = clientOptions.timeout + maxRetries = clientOptions.maxRetries + logLevel = clientOptions.logLevel + browserbaseApiKey = clientOptions.browserbaseApiKey + browserbaseProjectId = clientOptions.browserbaseProjectId + modelApiKey = clientOptions.modelApiKey + } + + /** + * The HTTP client to use in the SDK. + * + * Use the one published in `stagehand-java-client-okhttp` or implement your own. + * + * This class takes ownership of the client and closes it when closed. + */ + fun httpClient(httpClient: HttpClient) = apply { + this.httpClient = PhantomReachableClosingHttpClient(httpClient) + } + + /** + * Whether to throw an exception if any of the Jackson versions detected at runtime are + * incompatible with the SDK's minimum supported Jackson version (2.13.4). + * + * Defaults to true. Use extreme caution when disabling this option. There is no guarantee + * that the SDK will work correctly when using an incompatible Jackson version. + */ + fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply { + this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility + } + + /** + * The Jackson JSON mapper to use for serializing and deserializing JSON. + * + * Defaults to [com.browserbase.api.core.jsonMapper]. The default is usually sufficient and + * rarely needs to be overridden. + */ + fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper } + + /** + * The executor to use for running [AsyncStreamResponse.Handler] callbacks. + * + * Defaults to a dedicated cached thread pool. + * + * This class takes ownership of the executor and shuts it down, if possible, when closed. + */ + fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply { + this.streamHandlerExecutor = + if (streamHandlerExecutor is ExecutorService) + PhantomReachableExecutorService(streamHandlerExecutor) + else streamHandlerExecutor + } + + /** + * The interface to use for delaying execution, like during retries. + * + * This is primarily useful for using fake delays in tests. + * + * Defaults to real execution delays. + * + * This class takes ownership of the sleeper and closes it when closed. + */ + fun sleeper(sleeper: Sleeper) = apply { this.sleeper = PhantomReachableSleeper(sleeper) } + + /** + * The clock to use for operations that require timing, like retries. + * + * This is primarily useful for using a fake clock in tests. + * + * Defaults to [Clock.systemUTC]. + */ + fun clock(clock: Clock) = apply { this.clock = clock } + + /** + * The base URL to use for every request. + * + * Defaults to the production environment: `https://api.stagehand.browserbase.com`. + */ + fun baseUrl(baseUrl: String?) = apply { this.baseUrl = baseUrl } + + /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */ + fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull()) + + /** + * Whether to call `validate` on every response before returning it. + * + * Setting this to `true` is _not_ forwards compatible with new types from the API for + * existing fields. + * + * Defaults to false, which means the shape of the response will not be validated upfront. + * Instead, validation will only occur for the parts of the response that are accessed. + */ + fun responseValidation(responseValidation: Boolean) = apply { + this.responseValidation = responseValidation + } + + /** + * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding + * retries. + * + * Defaults to [Timeout.default]. + */ + fun timeout(timeout: Timeout) = apply { this.timeout = timeout } + + /** + * Sets the maximum time allowed for a complete HTTP call, not including retries. + * + * See [Timeout.request] for more details. + * + * For fine-grained control, pass a [Timeout] object. + */ + fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build()) + + /** + * The maximum number of times to retry failed requests, with a short exponential backoff + * between requests. + * + * Only the following error types are retried: + * - Connection errors (for example, due to a network connectivity problem) + * - 408 Request Timeout + * - 409 Conflict + * - 429 Rate Limit + * - 5xx Internal + * + * The API may also explicitly instruct the SDK to retry or not retry a request. + * + * Defaults to 2. + */ + fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries } + + /** + * The level at which to log request and response information. + * + * [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv]. + * + * Defaults to [LogLevel.fromEnv]. + */ + fun logLevel(logLevel: LogLevel) = apply { this.logLevel = logLevel } + + /** Your [Browserbase API Key](https://www.browserbase.com/settings) */ + fun browserbaseApiKey(browserbaseApiKey: String) = apply { + this.browserbaseApiKey = browserbaseApiKey + } + + /** + * Deprecated. Browserbase API keys are now project-scoped, so this value is no longer + * required. + */ + fun browserbaseProjectId(browserbaseProjectId: String?) = apply { + this.browserbaseProjectId = browserbaseProjectId + } + + /** + * Alias for calling [Builder.browserbaseProjectId] with + * `browserbaseProjectId.orElse(null)`. + */ + fun browserbaseProjectId(browserbaseProjectId: Optional) = + browserbaseProjectId(browserbaseProjectId.getOrNull()) + + /** Your LLM provider API key (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) */ + fun modelApiKey(modelApiKey: String) = apply { this.modelApiKey = modelApiKey } + + fun headers(headers: Headers) = apply { + this.headers.clear() + putAllHeaders(headers) + } + + fun headers(headers: Map>) = apply { + this.headers.clear() + putAllHeaders(headers) + } + + fun putHeader(name: String, value: String) = apply { headers.put(name, value) } + + fun putHeaders(name: String, values: Iterable) = apply { headers.put(name, values) } + + fun putAllHeaders(headers: Headers) = apply { this.headers.putAll(headers) } + + fun putAllHeaders(headers: Map>) = apply { + this.headers.putAll(headers) + } + + fun replaceHeaders(name: String, value: String) = apply { headers.replace(name, value) } + + fun replaceHeaders(name: String, values: Iterable) = apply { + headers.replace(name, values) + } + + fun replaceAllHeaders(headers: Headers) = apply { this.headers.replaceAll(headers) } + + fun replaceAllHeaders(headers: Map>) = apply { + this.headers.replaceAll(headers) + } + + fun removeHeaders(name: String) = apply { headers.remove(name) } + + fun removeAllHeaders(names: Set) = apply { headers.removeAll(names) } + + fun queryParams(queryParams: QueryParams) = apply { + this.queryParams.clear() + putAllQueryParams(queryParams) + } + + fun queryParams(queryParams: Map>) = apply { + this.queryParams.clear() + putAllQueryParams(queryParams) + } + + fun putQueryParam(key: String, value: String) = apply { queryParams.put(key, value) } + + fun putQueryParams(key: String, values: Iterable) = apply { + queryParams.put(key, values) + } + + fun putAllQueryParams(queryParams: QueryParams) = apply { + this.queryParams.putAll(queryParams) + } + + fun putAllQueryParams(queryParams: Map>) = apply { + this.queryParams.putAll(queryParams) + } + + fun replaceQueryParams(key: String, value: String) = apply { + queryParams.replace(key, value) + } + + fun replaceQueryParams(key: String, values: Iterable) = apply { + queryParams.replace(key, values) + } + + fun replaceAllQueryParams(queryParams: QueryParams) = apply { + this.queryParams.replaceAll(queryParams) + } + + fun replaceAllQueryParams(queryParams: Map>) = apply { + this.queryParams.replaceAll(queryParams) + } + + fun removeQueryParams(key: String) = apply { queryParams.remove(key) } + + fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) } + + fun timeout(): Timeout = timeout + + /** + * Updates configuration using system properties and environment variables. + * + * See this table for the available options: + * + * |Setter |System property |Environment variable |Required|Default value | + * |----------------------|--------------------------------|------------------------|--------|-----------------------------------------| + * |`browserbaseApiKey` |`stagehand.browserbaseApiKey` |`BROWSERBASE_API_KEY` |true |- | + * |`browserbaseProjectId`|`stagehand.browserbaseProjectId`|`BROWSERBASE_PROJECT_ID`|false |- | + * |`modelApiKey` |`stagehand.modelApiKey` |`MODEL_API_KEY` |true |- | + * |`baseUrl` |`stagehand.baseUrl` |`STAGEHAND_BASE_URL` |true |`"https://api.stagehand.browserbase.com"`| + * + * System properties take precedence over environment variables. + */ + fun fromEnv() = apply { + logLevel(LogLevel.fromEnv()) + (System.getProperty("stagehand.baseUrl") ?: System.getenv("STAGEHAND_BASE_URL"))?.let { + baseUrl(it) + } + (System.getProperty("stagehand.browserbaseApiKey") + ?: System.getenv("BROWSERBASE_API_KEY")) + ?.let { browserbaseApiKey(it) } + (System.getProperty("stagehand.browserbaseProjectId") + ?: System.getenv("BROWSERBASE_PROJECT_ID")) + ?.let { browserbaseProjectId(it) } + (System.getProperty("stagehand.modelApiKey") ?: System.getenv("MODEL_API_KEY"))?.let { + modelApiKey(it) + } + System.getenv("STAGEHAND_CUSTOM_HEADERS")?.let { customHeadersEnv -> + for (line in customHeadersEnv.split("\n")) { + val colon = line.indexOf(':') + if (colon >= 0) { + putHeader(line.substring(0, colon).trim(), line.substring(colon + 1).trim()) + } + } + } + } + + /** + * Returns an immutable instance of [ClientOptions]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .httpClient() + * .browserbaseApiKey() + * .modelApiKey() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): ClientOptions { + val httpClient = checkRequired("httpClient", httpClient) + val streamHandlerExecutor = + streamHandlerExecutor + ?: PhantomReachableExecutorService( + Executors.newCachedThreadPool( + object : ThreadFactory { + + private val threadFactory: ThreadFactory = + Executors.defaultThreadFactory() + private val count = AtomicLong(0) + + override fun newThread(runnable: Runnable): Thread = + threadFactory.newThread(runnable).also { + it.name = + "stagehand-stream-handler-thread-${count.getAndIncrement()}" + } + } + ) + ) + val sleeper = sleeper ?: PhantomReachableSleeper(DefaultSleeper()) + val browserbaseApiKey = checkRequired("browserbaseApiKey", browserbaseApiKey) + val modelApiKey = checkRequired("modelApiKey", modelApiKey) + + val headers = Headers.builder() + val queryParams = QueryParams.builder() + headers.put("X-Stainless-Lang", "java") + headers.put("X-Stainless-Arch", getOsArch()) + headers.put("X-Stainless-OS", getOsName()) + headers.put("X-Stainless-OS-Version", getOsVersion()) + 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()) + // We replace after all the default headers to allow end-users to overwrite them. + headers.replaceAll(this.headers.build()) + queryParams.replaceAll(this.queryParams.build()) + browserbaseApiKey.let { + if (!it.isEmpty()) { + headers.replace("x-bb-api-key", it) + } + } + browserbaseProjectId?.let { + if (!it.isEmpty()) { + headers.replace("x-bb-project-id", it) + } + } + modelApiKey.let { + if (!it.isEmpty()) { + headers.replace("x-model-api-key", it) + } + } + + return ClientOptions( + httpClient, + RetryingHttpClient.builder() + .httpClient( + LoggingHttpClient.builder() + .httpClient(httpClient) + .clock(clock) + .level(logLevel) + .build() + ) + .sleeper(sleeper) + .clock(clock) + .maxRetries(maxRetries) + .build(), + checkJacksonVersionCompatibility, + jsonMapper, + streamHandlerExecutor, + sleeper, + clock, + baseUrl, + headers.build(), + queryParams.build(), + responseValidation, + timeout, + maxRetries, + logLevel, + browserbaseApiKey, + browserbaseProjectId, + modelApiKey, + ) + } + } + + /** + * Closes these client options, relinquishing any underlying resources. + * + * This is purposefully not inherited from [AutoCloseable] because the client options are + * long-lived and usually should not be synchronously closed via try-with-resources. + * + * It's also usually not necessary to call this method at all. the default client automatically + * releases threads and connections if they remain idle, but if you are writing an application + * that needs to aggressively release unused resources, then you may call this method. + */ + fun close() { + httpClient.close() + (streamHandlerExecutor as? ExecutorService)?.shutdown() + sleeper.close() + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/DefaultSleeper.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/DefaultSleeper.kt new file mode 100644 index 0000000..7ea0449 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/DefaultSleeper.kt @@ -0,0 +1,28 @@ +package com.browserbase.api.core + +import java.time.Duration +import java.util.Timer +import java.util.TimerTask +import java.util.concurrent.CompletableFuture + +class DefaultSleeper : Sleeper { + + private val timer = Timer("DefaultSleeper", true) + + override fun sleep(duration: Duration) = Thread.sleep(duration.toMillis()) + + override fun sleepAsync(duration: Duration): CompletableFuture { + val future = CompletableFuture() + timer.schedule( + object : TimerTask() { + override fun run() { + future.complete(null) + } + }, + duration.toMillis(), + ) + return future + } + + override fun close() = timer.cancel() +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/LogLevel.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/LogLevel.kt new file mode 100644 index 0000000..f5a07a1 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/LogLevel.kt @@ -0,0 +1,33 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core + +/** The level at which to log request and response information. */ +enum class LogLevel { + /** No logging. */ + OFF, + /** Minimal request and response summary logs. No headers or bodies are logged. */ + INFO, + /** [INFO] logs plus details about request failures. */ + ERROR, + /** + * Full request and response logs. Sensitive headers are redacted, but sensitive data in request + * and response bodies may still be visible. + */ + DEBUG; + + /** Returns whether this level is at or higher than the given [level]. */ + fun shouldLog(level: LogLevel): Boolean = ordinal >= level.ordinal + + companion object { + + /** Returns a [LogLevel] based on the `STAGEHAND_LOG` environment variable. */ + fun fromEnv() = + when (System.getenv("STAGEHAND_LOG")?.lowercase()) { + "info" -> INFO + "error" -> ERROR + "debug" -> DEBUG + else -> OFF + } + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ObjectMappers.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ObjectMappers.kt new file mode 100644 index 0000000..65b7e72 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ObjectMappers.kt @@ -0,0 +1,180 @@ +@file:JvmName("ObjectMappers") + +package com.browserbase.api.core + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.cfg.CoercionAction +import com.fasterxml.jackson.databind.cfg.CoercionInputShape +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.type.LogicalType +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.kotlinModule +import java.io.InputStream +import java.time.DateTimeException +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoField + +fun jsonMapper(): JsonMapper = JSON_MAPPER + +private val JSON_MAPPER: JsonMapper = + JsonMapper.builder() + .addModule(kotlinModule()) + .addModule(Jdk8Module()) + .addModule(JavaTimeModule()) + .addModule( + SimpleModule() + .addSerializer(InputStreamSerializer) + .addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer()) + ) + .withCoercionConfig(LogicalType.Boolean) { + it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .setCoercion(CoercionInputShape.String, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) + } + .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) + } + .withCoercionConfig(LogicalType.Float) { + it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) + .setCoercion(CoercionInputShape.String, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) + } + .withCoercionConfig(LogicalType.Textual) { + it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .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) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .setCoercion(CoercionInputShape.String, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) + } + .withCoercionConfig(LogicalType.Collection) { + it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .setCoercion(CoercionInputShape.String, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) + } + .withCoercionConfig(LogicalType.Map) { + it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .setCoercion(CoercionInputShape.String, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) + } + .withCoercionConfig(LogicalType.POJO) { + it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .setCoercion(CoercionInputShape.String, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) + } + .serializationInclusion(JsonInclude.Include.NON_ABSENT) + .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) + .disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) + .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS) + .disable(MapperFeature.AUTO_DETECT_CREATORS) + .disable(MapperFeature.AUTO_DETECT_FIELDS) + .disable(MapperFeature.AUTO_DETECT_GETTERS) + .disable(MapperFeature.AUTO_DETECT_IS_GETTERS) + .disable(MapperFeature.AUTO_DETECT_SETTERS) + .build() + +/** A serializer that serializes [InputStream] to bytes. */ +private object InputStreamSerializer : BaseSerializer(InputStream::class) { + + private fun readResolve(): Any = InputStreamSerializer + + override fun serialize( + value: InputStream?, + gen: JsonGenerator?, + serializers: SerializerProvider?, + ) { + if (value == null) { + gen?.writeNull() + } else { + value.use { gen?.writeBinary(it.readBytes()) } + } + } +} + +/** + * A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes. + */ +private class LenientOffsetDateTimeDeserializer : + StdDeserializer(OffsetDateTime::class.java) { + + companion object { + + private val DATE_TIME_FORMATTERS = + listOf( + DateTimeFormatter.ISO_LOCAL_DATE_TIME, + DateTimeFormatter.ISO_LOCAL_DATE, + DateTimeFormatter.ISO_ZONED_DATE_TIME, + ) + } + + override fun logicalType(): LogicalType = LogicalType.DateTime + + override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime { + val exceptions = mutableListOf() + + for (formatter in DATE_TIME_FORMATTERS) { + try { + val temporal = formatter.parse(p.text) + + return when { + !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) + } + } + + throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply { + exceptions.forEach { addSuppressed(it) } + } + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Params.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Params.kt new file mode 100644 index 0000000..74e0357 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Params.kt @@ -0,0 +1,16 @@ +package com.browserbase.api.core + +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.QueryParams + +/** An interface representing parameters passed to a service method. */ +interface Params { + /** The full set of headers in the parameters, including both fixed and additional headers. */ + fun _headers(): Headers + + /** + * The full set of query params in the parameters, including both fixed and additional query + * params. + */ + fun _queryParams(): QueryParams +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PhantomReachable.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PhantomReachable.kt new file mode 100644 index 0000000..12cba3f --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PhantomReachable.kt @@ -0,0 +1,56 @@ +@file:JvmName("PhantomReachable") + +package com.browserbase.api.core + +import com.browserbase.api.errors.StagehandException +import java.lang.reflect.InvocationTargetException + +/** + * Closes [closeable] when [observed] becomes only phantom reachable. + * + * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions. + */ +@JvmSynthetic +internal fun closeWhenPhantomReachable(observed: Any, closeable: AutoCloseable) { + check(observed !== closeable) { + "`observed` cannot be the same object as `closeable` because it would never become phantom reachable" + } + closeWhenPhantomReachable(observed, closeable::close) +} + +/** + * Calls [close] when [observed] becomes only phantom reachable. + * + * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions. + */ +@JvmSynthetic +internal fun closeWhenPhantomReachable(observed: Any, close: () -> Unit) { + closeWhenPhantomReachable?.let { it(observed, close) } +} + +private val closeWhenPhantomReachable: ((Any, () -> Unit) -> Unit)? by lazy { + try { + val cleanerClass = Class.forName("java.lang.ref.Cleaner") + val cleanerCreate = cleanerClass.getMethod("create") + val cleanerRegister = + cleanerClass.getMethod("register", Any::class.java, Runnable::class.java) + val cleanerObject = cleanerCreate.invoke(null); + + { observed, close -> + try { + cleanerRegister.invoke(cleanerObject, observed, Runnable { close() }) + } catch (e: ReflectiveOperationException) { + if (e is InvocationTargetException) { + when (val cause = e.cause) { + is RuntimeException, + is Error -> throw cause + } + } + throw StagehandException("Unexpected reflective invocation failure", e) + } + } + } catch (e: ReflectiveOperationException) { + // We're running Java 8, which has no Cleaner. + null + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PhantomReachableExecutorService.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PhantomReachableExecutorService.kt new file mode 100644 index 0000000..7fb24a5 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PhantomReachableExecutorService.kt @@ -0,0 +1,58 @@ +package com.browserbase.api.core + +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit + +/** + * A delegating wrapper around an [ExecutorService] that shuts it down once it's only phantom + * reachable. + * + * This class ensures the [ExecutorService] is shut down even if the user forgets to do it. + */ +internal class PhantomReachableExecutorService(private val executorService: ExecutorService) : + ExecutorService { + init { + closeWhenPhantomReachable(this) { executorService.shutdown() } + } + + override fun execute(command: Runnable) = executorService.execute(command) + + override fun shutdown() = executorService.shutdown() + + override fun shutdownNow(): MutableList = executorService.shutdownNow() + + override fun isShutdown(): Boolean = executorService.isShutdown + + override fun isTerminated(): Boolean = executorService.isTerminated + + override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean = + executorService.awaitTermination(timeout, unit) + + override fun submit(task: Callable): Future = executorService.submit(task) + + override fun submit(task: Runnable, result: T): Future = + executorService.submit(task, result) + + override fun submit(task: Runnable): Future<*> = executorService.submit(task) + + override fun invokeAll( + tasks: MutableCollection> + ): MutableList> = executorService.invokeAll(tasks) + + override fun invokeAll( + tasks: MutableCollection>, + timeout: Long, + unit: TimeUnit, + ): MutableList> = executorService.invokeAll(tasks, timeout, unit) + + override fun invokeAny(tasks: MutableCollection>): T = + executorService.invokeAny(tasks) + + override fun invokeAny( + tasks: MutableCollection>, + timeout: Long, + unit: TimeUnit, + ): T = executorService.invokeAny(tasks, timeout, unit) +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PhantomReachableSleeper.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PhantomReachableSleeper.kt new file mode 100644 index 0000000..172760d --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PhantomReachableSleeper.kt @@ -0,0 +1,23 @@ +package com.browserbase.api.core + +import java.time.Duration +import java.util.concurrent.CompletableFuture + +/** + * A delegating wrapper around a [Sleeper] that closes it once it's only phantom reachable. + * + * This class ensures the [Sleeper] is closed even if the user forgets to do it. + */ +internal class PhantomReachableSleeper(private val sleeper: Sleeper) : Sleeper { + + init { + closeWhenPhantomReachable(this, sleeper) + } + + override fun sleep(duration: Duration) = sleeper.sleep(duration) + + override fun sleepAsync(duration: Duration): CompletableFuture = + sleeper.sleepAsync(duration) + + override fun close() = sleeper.close() +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PrepareRequest.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PrepareRequest.kt new file mode 100644 index 0000000..89ffa2d --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/PrepareRequest.kt @@ -0,0 +1,24 @@ +@file:JvmName("PrepareRequest") + +package com.browserbase.api.core + +import com.browserbase.api.core.http.HttpRequest +import java.util.concurrent.CompletableFuture + +@JvmSynthetic +internal fun HttpRequest.prepare(clientOptions: ClientOptions, params: Params): HttpRequest = + toBuilder() + .putAllQueryParams(clientOptions.queryParams) + .replaceAllQueryParams(params._queryParams()) + .putAllHeaders(clientOptions.headers) + .replaceAllHeaders(params._headers()) + .build() + +@JvmSynthetic +internal fun HttpRequest.prepareAsync( + clientOptions: ClientOptions, + params: Params, +): CompletableFuture = + // This async version exists to make it easier to add async specific preparation logic in the + // future. + CompletableFuture.completedFuture(prepare(clientOptions, params)) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Properties.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Properties.kt new file mode 100644 index 0000000..936e15a --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Properties.kt @@ -0,0 +1,42 @@ +@file:JvmName("Properties") + +package com.browserbase.api.core + +import com.browserbase.api.client.StagehandClient + +fun getOsArch(): String { + val osArch = System.getProperty("os.arch") + + return when (osArch) { + null -> "unknown" + "i386", + "x32", + "x86" -> "x32" + "amd64", + "x86_64" -> "x64" + "arm" -> "arm" + "aarch64" -> "arm64" + else -> "other:$osArch" + } +} + +fun getOsName(): String { + val osName = System.getProperty("os.name") + val vendorUrl = System.getProperty("java.vendor.url") + + return when { + osName == null -> "Unknown" + osName.startsWith("Linux") && vendorUrl == "http://www.android.com/" -> "Android" + osName.startsWith("Linux") -> "Linux" + osName.startsWith("Mac OS") -> "MacOS" + osName.startsWith("Windows") -> "Windows" + else -> "Other:$osName" + } +} + +fun getOsVersion(): String = System.getProperty("os.version", "unknown") ?: "unknown" + +fun getPackageVersion(): String = + StagehandClient::class.java.`package`?.implementationVersion ?: "unknown" + +fun getJavaVersion(): String = System.getProperty("java.version", "unknown") ?: "unknown" diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/RequestOptions.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/RequestOptions.kt new file mode 100644 index 0000000..795cdd5 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/RequestOptions.kt @@ -0,0 +1,55 @@ +package com.browserbase.api.core + +import java.time.Duration + +class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Timeout?) { + + companion object { + + private val NONE = builder().build() + + @JvmStatic fun none() = NONE + + @JvmSynthetic + internal fun from(clientOptions: ClientOptions): RequestOptions = + builder() + .responseValidation(clientOptions.responseValidation) + .timeout(clientOptions.timeout) + .build() + + @JvmStatic fun builder() = Builder() + } + + fun applyDefaults(options: RequestOptions): RequestOptions = + RequestOptions( + responseValidation = responseValidation ?: options.responseValidation, + timeout = + if (options.timeout != null && timeout != null) timeout.assign(options.timeout) + else timeout ?: options.timeout, + ) + + class Builder internal constructor() { + + private var responseValidation: Boolean? = null + private var timeout: Timeout? = null + + /** + * Whether to call `validate` on the response before returning it. + * + * Setting this to `true` is _not_ forwards compatible with new types from the API for + * existing fields. + * + * Defaults to false, which means the shape of the response will not be validated upfront. + * Instead, validation will only occur for the parts of the response that are accessed. + */ + fun responseValidation(responseValidation: Boolean) = apply { + this.responseValidation = responseValidation + } + + fun timeout(timeout: Timeout) = apply { this.timeout = timeout } + + fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build()) + + fun build(): RequestOptions = RequestOptions(responseValidation, timeout) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Sleeper.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Sleeper.kt new file mode 100644 index 0000000..7c5e93a --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Sleeper.kt @@ -0,0 +1,21 @@ +package com.browserbase.api.core + +import java.time.Duration +import java.util.concurrent.CompletableFuture + +/** + * An interface for delaying execution for a specified amount of time. + * + * Useful for testing and cleaning up resources. + */ +interface Sleeper : AutoCloseable { + + /** Synchronously pauses execution for the given [duration]. */ + fun sleep(duration: Duration) + + /** Asynchronously pauses execution for the given [duration]. */ + fun sleepAsync(duration: Duration): CompletableFuture + + /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */ + override fun close() +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Timeout.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Timeout.kt new file mode 100644 index 0000000..d5618ce --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Timeout.kt @@ -0,0 +1,171 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core + +import java.time.Duration +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** A class containing timeouts for various processing phases of a request. */ +class Timeout +private constructor( + private val connect: Duration?, + private val read: Duration?, + private val write: Duration?, + private val request: Duration?, +) { + + /** + * The maximum time allowed to establish a connection with a host. + * + * A value of [Duration.ZERO] means there's no timeout. + * + * Defaults to `Duration.ofMinutes(1)`. + */ + fun connect(): Duration = connect ?: Duration.ofMinutes(1) + + /** + * The maximum time allowed between two data packets when waiting for the server’s response. + * + * A value of [Duration.ZERO] means there's no timeout. + * + * Defaults to `request()`. + */ + fun read(): Duration = read ?: request() + + /** + * The maximum time allowed between two data packets when sending the request to the server. + * + * A value of [Duration.ZERO] means there's no timeout. + * + * Defaults to `request()`. + */ + fun write(): Duration = write ?: request() + + /** + * The maximum time allowed for a complete HTTP call, not including retries. + * + * This includes resolving DNS, connecting, writing the request body, server processing, as well + * as reading the response body. + * + * A value of [Duration.ZERO] means there's no timeout. + * + * Defaults to `Duration.ofMinutes(1)`. + */ + fun request(): Duration = request ?: Duration.ofMinutes(1) + + fun toBuilder() = Builder().from(this) + + companion object { + + @JvmStatic fun default() = builder().build() + + /** Returns a mutable builder for constructing an instance of [Timeout]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Timeout]. */ + class Builder internal constructor() { + + private var connect: Duration? = null + private var read: Duration? = null + private var write: Duration? = null + private var request: Duration? = null + + @JvmSynthetic + internal fun from(timeout: Timeout) = apply { + connect = timeout.connect + read = timeout.read + write = timeout.write + request = timeout.request + } + + /** + * The maximum time allowed to establish a connection with a host. + * + * A value of [Duration.ZERO] means there's no timeout. + * + * Defaults to `Duration.ofMinutes(1)`. + */ + fun connect(connect: Duration?) = apply { this.connect = connect } + + /** Alias for calling [Builder.connect] with `connect.orElse(null)`. */ + fun connect(connect: Optional) = connect(connect.getOrNull()) + + /** + * The maximum time allowed between two data packets when waiting for the server’s response. + * + * A value of [Duration.ZERO] means there's no timeout. + * + * Defaults to `request()`. + */ + fun read(read: Duration?) = apply { this.read = read } + + /** Alias for calling [Builder.read] with `read.orElse(null)`. */ + fun read(read: Optional) = read(read.getOrNull()) + + /** + * The maximum time allowed between two data packets when sending the request to the server. + * + * A value of [Duration.ZERO] means there's no timeout. + * + * Defaults to `request()`. + */ + fun write(write: Duration?) = apply { this.write = write } + + /** Alias for calling [Builder.write] with `write.orElse(null)`. */ + fun write(write: Optional) = write(write.getOrNull()) + + /** + * The maximum time allowed for a complete HTTP call, not including retries. + * + * This includes resolving DNS, connecting, writing the request body, server processing, as + * well as reading the response body. + * + * A value of [Duration.ZERO] means there's no timeout. + * + * Defaults to `Duration.ofMinutes(1)`. + */ + fun request(request: Duration?) = apply { this.request = request } + + /** Alias for calling [Builder.request] with `request.orElse(null)`. */ + fun request(request: Optional) = request(request.getOrNull()) + + /** + * Returns an immutable instance of [Timeout]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Timeout = Timeout(connect, read, write, request) + } + + @JvmSynthetic + internal fun assign(target: Timeout): Timeout = + target + .toBuilder() + .apply { + connect?.let(this::connect) + read?.let(this::read) + write?.let(this::write) + request?.let(this::request) + } + .build() + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Timeout && + connect == other.connect && + read == other.read && + write == other.write && + request == other.request + } + + override fun hashCode(): Int = Objects.hash(connect, read, write, request) + + override fun toString() = + "Timeout{connect=$connect, read=$read, write=$write, request=$request}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Utils.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Utils.kt new file mode 100644 index 0000000..ee3da60 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Utils.kt @@ -0,0 +1,121 @@ +@file:JvmName("Utils") + +package com.browserbase.api.core + +import com.browserbase.api.errors.StagehandInvalidDataException +import java.util.Collections +import java.util.SortedMap +import java.util.SortedSet +import java.util.concurrent.CompletableFuture +import java.util.concurrent.locks.Lock + +@JvmSynthetic +internal fun T?.getOrThrow(name: String): T = + this ?: throw StagehandInvalidDataException("`${name}` is not present") + +@JvmSynthetic +internal fun List.toImmutable(): List = + if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList()) + +@JvmSynthetic +internal fun > SortedSet.toImmutable(): SortedSet = + if (isEmpty()) Collections.emptySortedSet() + else Collections.unmodifiableSortedSet(toSortedSet(comparator() ?: Comparator.naturalOrder())) + +@JvmSynthetic +internal fun Map.toImmutable(): Map = + if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap()) + +@JvmSynthetic internal fun immutableEmptyMap(): Map = Collections.emptyMap() + +@JvmSynthetic +internal fun , V> SortedMap.toImmutable(): SortedMap = + if (isEmpty()) Collections.emptySortedMap() + else Collections.unmodifiableSortedMap(toSortedMap(comparator())) + +/** + * Returns all elements that yield the largest value for the given function, or an empty list if + * there are zero elements. + * + * This is similar to [Sequence.maxByOrNull] except it returns _all_ elements that yield the largest + * value; not just the first one. + */ +@JvmSynthetic +internal fun > Sequence.allMaxBy(selector: (T) -> R): List { + var maxValue: R? = null + val maxElements = mutableListOf() + + val iterator = iterator() + while (iterator.hasNext()) { + val element = iterator.next() + val value = selector(element) + if (maxValue == null || value > maxValue) { + maxValue = value + maxElements.clear() + maxElements.add(element) + } else if (value == maxValue) { + maxElements.add(element) + } + } + + return maxElements +} + +/** + * Returns whether [this] is equal to [other]. + * + * This differs from [Object.equals] because it also deeply equates arrays based on their contents, + * even when there are arrays directly nested within other arrays. + */ +@JvmSynthetic +internal infix fun Any?.contentEquals(other: Any?): Boolean = + arrayOf(this).contentDeepEquals(arrayOf(other)) + +/** + * Returns a hash of the given sequence of [values]. + * + * This differs from [java.util.Objects.hash] because it also deeply hashes arrays based on their + * contents, even when there are arrays directly nested within other arrays. + */ +@JvmSynthetic internal fun contentHash(vararg values: Any?): Int = values.contentDeepHashCode() + +/** + * Returns a [String] representation of [this]. + * + * This differs from [Object.toString] because it also deeply stringifies arrays based on their + * contents, even when there are arrays directly nested within other arrays. + */ +@JvmSynthetic +internal fun Any?.contentToString(): String { + var string = arrayOf(this).contentDeepToString() + if (string.startsWith('[')) { + string = string.substring(1) + } + if (string.endsWith(']')) { + string = string.substring(0, string.length - 1) + } + return string +} + +internal interface Enum + +/** + * Executes the given [action] while holding the lock, returning a [CompletableFuture] with the + * result. + * + * @param action The asynchronous action to execute while holding the lock + * @return A [CompletableFuture] that completes with the result of the action + */ +@JvmSynthetic +internal fun Lock.withLockAsync(action: () -> CompletableFuture): CompletableFuture { + lock() + val future = + try { + action() + } catch (e: Throwable) { + unlock() + throw e + } + future.whenComplete { _, _ -> unlock() } + return future +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Values.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Values.kt new file mode 100644 index 0000000..db0102a --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Values.kt @@ -0,0 +1,723 @@ +package com.browserbase.api.core + +import com.browserbase.api.errors.StagehandInvalidDataException +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.BeanProperty +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.node.JsonNodeType.ARRAY +import com.fasterxml.jackson.databind.node.JsonNodeType.BINARY +import com.fasterxml.jackson.databind.node.JsonNodeType.BOOLEAN +import com.fasterxml.jackson.databind.node.JsonNodeType.MISSING +import com.fasterxml.jackson.databind.node.JsonNodeType.NULL +import com.fasterxml.jackson.databind.node.JsonNodeType.NUMBER +import com.fasterxml.jackson.databind.node.JsonNodeType.OBJECT +import com.fasterxml.jackson.databind.node.JsonNodeType.POJO +import com.fasterxml.jackson.databind.node.JsonNodeType.STRING +import com.fasterxml.jackson.databind.ser.std.NullSerializer +import java.io.InputStream +import java.util.Objects +import java.util.Optional + +/** + * A class representing a serializable JSON field. + * + * It can either be a [KnownValue] value of type [T], matching the type the SDK expects, or an + * arbitrary JSON value that bypasses the type system (via [JsonValue]). + */ +@JsonDeserialize(using = JsonField.Deserializer::class) +sealed class JsonField { + + /** + * Returns whether this field is missing, which means it will be omitted from the serialized + * JSON entirely. + */ + fun isMissing(): Boolean = this is JsonMissing + + /** Whether this field is explicitly set to `null`. */ + fun isNull(): Boolean = this is JsonNull + + /** + * Returns an [Optional] containing this field's "known" value, meaning it matches the type the + * SDK expects, or an empty [Optional] if this field contains an arbitrary [JsonValue]. + * + * This is the opposite of [asUnknown]. + */ + fun asKnown(): + Optional< + // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that. + @UnsafeVariance + T + > = Optional.ofNullable((this as? KnownValue)?.value) + + /** + * Returns an [Optional] containing this field's arbitrary [JsonValue], meaning it mismatches + * the type the SDK expects, or an empty [Optional] if this field contains a "known" value. + * + * This is the opposite of [asKnown]. + */ + fun asUnknown(): Optional = Optional.ofNullable(this as? JsonValue) + + /** + * Returns an [Optional] containing this field's boolean value, or an empty [Optional] if it + * doesn't contain a boolean. + * + * This method checks for both a [KnownValue] containing a boolean and for [JsonBoolean]. + */ + fun asBoolean(): Optional = + when (this) { + is JsonBoolean -> Optional.of(value) + is KnownValue -> Optional.ofNullable(value as? Boolean) + else -> Optional.empty() + } + + /** + * Returns an [Optional] containing this field's numerical value, or an empty [Optional] if it + * doesn't contain a number. + * + * This method checks for both a [KnownValue] containing a number and for [JsonNumber]. + */ + fun asNumber(): Optional = + when (this) { + is JsonNumber -> Optional.of(value) + is KnownValue -> Optional.ofNullable(value as? Number) + else -> Optional.empty() + } + + /** + * Returns an [Optional] containing this field's string value, or an empty [Optional] if it + * doesn't contain a string. + * + * This method checks for both a [KnownValue] containing a string and for [JsonString]. + */ + fun asString(): Optional = + when (this) { + is JsonString -> Optional.of(value) + is KnownValue -> Optional.ofNullable(value as? String) + else -> Optional.empty() + } + + fun asStringOrThrow(): String = + asString().orElseThrow { StagehandInvalidDataException("Value is not a string") } + + /** + * Returns an [Optional] containing this field's list value, or an empty [Optional] if it + * doesn't contain a list. + * + * This method checks for both a [KnownValue] containing a list and for [JsonArray]. + */ + fun asArray(): Optional> = + when (this) { + is JsonArray -> Optional.of(values) + is KnownValue -> + Optional.ofNullable( + (value as? List<*>)?.map { + try { + JsonValue.from(it) + } catch (e: IllegalArgumentException) { + // The known value is a list, but not all values are convertible to + // `JsonValue`. + return Optional.empty() + } + } + ) + else -> Optional.empty() + } + + /** + * Returns an [Optional] containing this field's map value, or an empty [Optional] if it doesn't + * contain a map. + * + * This method checks for both a [KnownValue] containing a map and for [JsonObject]. + */ + fun asObject(): Optional> = + when (this) { + is JsonObject -> Optional.of(values) + is KnownValue -> + Optional.ofNullable( + (value as? Map<*, *>) + ?.map { (key, value) -> + if (key !is String) { + return Optional.empty() + } + + val jsonValue = + try { + JsonValue.from(value) + } catch (e: IllegalArgumentException) { + // The known value is a map, but not all items are convertible + // to `JsonValue`. + return Optional.empty() + } + + key to jsonValue + } + ?.toMap() + ) + else -> Optional.empty() + } + + @JvmSynthetic + internal fun getRequired(name: String): T = + when (this) { + is KnownValue -> value + is JsonMissing -> throw StagehandInvalidDataException("`$name` is not set") + is JsonNull -> throw StagehandInvalidDataException("`$name` is null") + else -> throw StagehandInvalidDataException("`$name` is invalid, received $this") + } + + @JvmSynthetic + internal fun getOptional( + name: String + ): Optional< + // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that. + @UnsafeVariance + T + > = + when (this) { + is KnownValue -> Optional.of(value) + is JsonMissing, + is JsonNull -> Optional.empty() + else -> throw StagehandInvalidDataException("`$name` is invalid, received $this") + } + + @JvmSynthetic + internal fun map(transform: (T) -> R): JsonField = + when (this) { + is KnownValue -> KnownValue.of(transform(value)) + is JsonValue -> this + } + + @JvmSynthetic internal fun accept(consume: (T) -> Unit) = asKnown().ifPresent(consume) + + /** Returns the result of calling the [visitor] method corresponding to this field's state. */ + fun accept(visitor: Visitor): R = + when (this) { + is KnownValue -> visitor.visitKnown(value) + is JsonValue -> accept(visitor as JsonValue.Visitor) + } + + /** + * An interface that defines how to map each possible state of a `JsonField` to a value of + * type [R]. + */ + interface Visitor : JsonValue.Visitor { + + fun visitKnown(value: T): R = visitDefault() + } + + companion object { + + /** Returns a [JsonField] containing the given "known" [value]. */ + @JvmStatic fun of(value: T): JsonField = KnownValue.of(value) + + /** + * Returns a [JsonField] containing the given "known" [value], or [JsonNull] if [value] is + * null. + */ + @JvmStatic + fun ofNullable(value: T?): JsonField = + when (value) { + null -> JsonNull.of() + else -> KnownValue.of(value) + } + } + + /** + * This class is a Jackson filter that can be used to exclude missing properties from objects. + * This filter should not be used directly and should instead use the @ExcludeMissing + * annotation. + */ + class IsMissing { + + override fun equals(other: Any?): Boolean = other is JsonMissing + + override fun hashCode(): Int = Objects.hash() + } + + class Deserializer(private val type: JavaType? = null) : + BaseDeserializer>(JsonField::class) { + + override fun createContextual( + context: DeserializationContext, + property: BeanProperty?, + ): JsonDeserializer> = Deserializer(context.contextualType?.containedType(0)) + + override fun ObjectCodec.deserialize(node: JsonNode): JsonField<*> = + type?.let { tryDeserialize(node, type) }?.let { of(it) } + ?: JsonValue.fromJsonNode(node) + + override fun getNullValue(context: DeserializationContext): JsonField<*> = JsonNull.of() + } +} + +/** + * A class representing an arbitrary JSON value. + * + * It is immutable and assignable to any [JsonField], regardless of its expected type (i.e. its + * generic type argument). + */ +@JsonDeserialize(using = JsonValue.Deserializer::class) +sealed class JsonValue : JsonField() { + + fun convert(type: TypeReference): R? = JSON_MAPPER.convertValue(this, type) + + fun convert(type: Class): R? = JSON_MAPPER.convertValue(this, type) + + /** Returns the result of calling the [visitor] method corresponding to this value's variant. */ + fun accept(visitor: Visitor): R = + when (this) { + is JsonMissing -> visitor.visitMissing() + is JsonNull -> visitor.visitNull() + is JsonBoolean -> visitor.visitBoolean(value) + is JsonNumber -> visitor.visitNumber(value) + is JsonString -> visitor.visitString(value) + is JsonArray -> visitor.visitArray(values) + is JsonObject -> visitor.visitObject(values) + } + + /** + * An interface that defines how to map each variant state of a [JsonValue] to a value of type + * [R]. + */ + interface Visitor { + + fun visitNull(): R = visitDefault() + + fun visitMissing(): R = visitDefault() + + fun visitBoolean(value: Boolean): R = visitDefault() + + fun visitNumber(value: Number): R = visitDefault() + + fun visitString(value: String): R = visitDefault() + + fun visitArray(values: List): R = visitDefault() + + fun visitObject(values: Map): R = visitDefault() + + /** + * The default implementation for unimplemented visitor methods. + * + * @throws IllegalArgumentException in the default implementation. + */ + fun visitDefault(): R = throw IllegalArgumentException("Unexpected value") + } + + companion object { + + private val JSON_MAPPER = jsonMapper() + + /** + * Converts the given [value] to a [JsonValue]. + * + * This method works best on primitive types, [List] values, [Map] values, and nested + * combinations of these. For example: + * ```java + * // Create primitive JSON values + * JsonValue nullValue = JsonValue.from(null); + * JsonValue booleanValue = JsonValue.from(true); + * JsonValue numberValue = JsonValue.from(42); + * JsonValue stringValue = JsonValue.from("Hello World!"); + * + * // Create a JSON array value equivalent to `["Hello", "World"]` + * JsonValue arrayValue = JsonValue.from(List.of("Hello", "World")); + * + * // Create a JSON object value equivalent to `{ "a": 1, "b": 2 }` + * JsonValue objectValue = JsonValue.from(Map.of( + * "a", 1, + * "b", 2 + * )); + * + * // Create an arbitrarily nested JSON equivalent to: + * // { + * // "a": [1, 2], + * // "b": [3, 4] + * // } + * JsonValue complexValue = JsonValue.from(Map.of( + * "a", List.of(1, 2), + * "b", List.of(3, 4) + * )); + * ``` + * + * @throws IllegalArgumentException if [value] is not JSON serializable. + */ + @JvmStatic + fun from(value: Any?): JsonValue = + when (value) { + null -> JsonNull.of() + is JsonValue -> value + else -> JSON_MAPPER.convertValue(value, JsonValue::class.java) + } + + /** + * Returns a [JsonValue] converted from the given Jackson [JsonNode]. + * + * @throws IllegalStateException for unsupported node types. + */ + @JvmStatic + fun fromJsonNode(node: JsonNode): JsonValue = + when (node.nodeType) { + MISSING -> JsonMissing.of() + NULL -> JsonNull.of() + BOOLEAN -> JsonBoolean.of(node.booleanValue()) + NUMBER -> JsonNumber.of(node.numberValue()) + STRING -> JsonString.of(node.textValue()) + ARRAY -> + JsonArray.of(node.elements().asSequence().map { fromJsonNode(it) }.toList()) + OBJECT -> + JsonObject.of( + node.fields().asSequence().map { it.key to fromJsonNode(it.value) }.toMap() + ) + BINARY, + POJO, + null -> throw IllegalStateException("Unexpected JsonNode type: ${node.nodeType}") + } + } + + class Deserializer : BaseDeserializer(JsonValue::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): JsonValue = fromJsonNode(node) + + override fun getNullValue(context: DeserializationContext?): JsonValue = JsonNull.of() + } +} + +/** + * A class representing a "known" JSON serializable value of type [T], matching the type the SDK + * expects. + * + * It is assignable to `JsonField`. + */ +class KnownValue +private constructor( + @com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: T +) : JsonField() { + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is KnownValue<*> && value contentEquals other.value + } + + override fun hashCode() = contentHash(value) + + override fun toString() = value.contentToString() + + companion object { + + /** Returns a [KnownValue] containing the given [value]. */ + @JsonCreator @JvmStatic fun of(value: T) = KnownValue(value) + } +} + +/** + * A [JsonValue] representing an omitted JSON field. + * + * An instance of this class will cause a JSON field to be omitted from the serialized JSON + * entirely. + */ +@JsonSerialize(using = JsonMissing.Serializer::class) +class JsonMissing : JsonValue() { + + override fun toString() = "" + + companion object { + + private val INSTANCE: JsonMissing = JsonMissing() + + /** Returns the singleton instance of [JsonMissing]. */ + @JvmStatic fun of() = INSTANCE + } + + class Serializer : BaseSerializer(JsonMissing::class) { + + override fun serialize( + value: JsonMissing, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + throw IllegalStateException("JsonMissing cannot be serialized") + } + } +} + +/** A [JsonValue] representing a JSON `null` value. */ +@JsonSerialize(using = NullSerializer::class) +class JsonNull : JsonValue() { + + override fun toString() = "null" + + companion object { + + private val INSTANCE: JsonNull = JsonNull() + + /** Returns the singleton instance of [JsonMissing]. */ + @JsonCreator @JvmStatic fun of() = INSTANCE + } +} + +/** A [JsonValue] representing a JSON boolean value. */ +class JsonBoolean +private constructor( + @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Boolean +) : JsonValue() { + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is JsonBoolean && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + + companion object { + + /** Returns a [JsonBoolean] containing the given [value]. */ + @JsonCreator @JvmStatic fun of(value: Boolean) = JsonBoolean(value) + } +} + +/** A [JsonValue] representing a JSON number value. */ +class JsonNumber +private constructor( + @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Number +) : JsonValue() { + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is JsonNumber && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + + companion object { + + /** Returns a [JsonNumber] containing the given [value]. */ + @JsonCreator @JvmStatic fun of(value: Number) = JsonNumber(value) + } +} + +/** A [JsonValue] representing a JSON string value. */ +class JsonString +private constructor( + @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: String +) : JsonValue() { + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is JsonString && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value + + companion object { + + /** Returns a [JsonString] containing the given [value]. */ + @JsonCreator @JvmStatic fun of(value: String) = JsonString(value) + } +} + +/** A [JsonValue] representing a JSON array value. */ +class JsonArray +private constructor( + @get:com.fasterxml.jackson.annotation.JsonValue + @get:JvmName("values") + val values: List +) : JsonValue() { + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is JsonArray && values == other.values + } + + override fun hashCode() = values.hashCode() + + override fun toString() = values.toString() + + companion object { + + /** Returns a [JsonArray] containing the given [values]. */ + @JsonCreator @JvmStatic fun of(values: List) = JsonArray(values.toImmutable()) + } +} + +/** A [JsonValue] representing a JSON object value. */ +class JsonObject +private constructor( + @get:com.fasterxml.jackson.annotation.JsonValue + @get:JvmName("values") + val values: Map +) : JsonValue() { + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is JsonObject && values == other.values + } + + override fun hashCode() = values.hashCode() + + override fun toString() = values.toString() + + companion object { + + /** Returns a [JsonObject] containing the given [values]. */ + @JsonCreator + @JvmStatic + fun of(values: Map) = JsonObject(values.toImmutable()) + } +} + +/** A Jackson annotation for excluding fields set to [JsonMissing] from the serialized JSON. */ +@JacksonAnnotationsInside +@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = JsonField.IsMissing::class) +annotation class ExcludeMissing + +/** A class representing a field in a `multipart/form-data` request. */ +class MultipartField +private constructor( + /** A [JsonField] value, which will be serialized to zero or more parts. */ + @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: JsonField, + /** A content type for the serialized parts. */ + @get:JvmName("contentType") val contentType: String, + private val filename: String?, +) { + + companion object { + + /** + * Returns a [MultipartField] containing the given [value] as a [KnownValue]. + * + * [contentType] will be set to `application/octet-stream` if [value] is binary data, or + * `text/plain; charset=utf-8` otherwise. + */ + @JvmStatic fun of(value: T?) = builder().value(value).build() + + /** + * Returns a [MultipartField] containing the given [value]. + * + * [contentType] will be set to `application/octet-stream` if [value] is binary data, or + * `text/plain; charset=utf-8` otherwise. + */ + @JvmStatic fun of(value: JsonField) = builder().value(value).build() + + /** + * Returns a mutable builder for constructing an instance of [MultipartField]. + * + * The following fields are required: + * ```java + * .value() + * ``` + * + * If [contentType] is unset, then it will be set to `application/octet-stream` if [value] + * is binary data, or `text/plain; charset=utf-8` otherwise. + */ + @JvmStatic fun builder() = Builder() + } + + /** Returns the filename directive that will be included in the serialized field. */ + fun filename(): Optional = Optional.ofNullable(filename) + + @JvmSynthetic + internal fun map(transform: (T) -> R): MultipartField = + builder().value(value.map(transform)).contentType(contentType).filename(filename).build() + + /** A builder for [MultipartField]. */ + class Builder internal constructor() { + + private var value: JsonField? = null + private var contentType: String? = null + private var filename: String? = null + + fun value(value: JsonField) = apply { this.value = value } + + fun value(value: T?) = value(JsonField.ofNullable(value)) + + fun contentType(contentType: String) = apply { this.contentType = contentType } + + fun filename(filename: String?) = apply { this.filename = filename } + + /** Alias for calling [Builder.filename] with `filename.orElse(null)`. */ + fun filename(filename: Optional) = filename(filename.orElse(null)) + + /** + * Returns an immutable instance of [MultipartField]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .value() + * ``` + * + * If [contentType] is unset, then it will be set to `application/octet-stream` if [value] + * is binary data, or `text/plain; charset=utf-8` otherwise. + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): MultipartField { + val value = checkRequired("value", value) + return MultipartField( + value, + contentType + ?: if ( + value is KnownValue && + (value.value is InputStream || value.value is ByteArray) + ) + "application/octet-stream" + else "text/plain; charset=utf-8", + filename, + ) + } + } + + private val hashCode: Int by lazy { contentHash(value, contentType, filename) } + + override fun hashCode(): Int = hashCode + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is MultipartField<*> && + value == other.value && + contentType == other.contentType && + filename == other.filename + } + + override fun toString(): String = + "MultipartField{value=$value, contentType=$contentType, filename=$filename}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/ErrorHandler.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/ErrorHandler.kt new file mode 100644 index 0000000..bd5fff6 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/ErrorHandler.kt @@ -0,0 +1,84 @@ +// File generated from our OpenAPI spec by Stainless. + +@file:JvmName("ErrorHandler") + +package com.browserbase.api.core.handlers + +import com.browserbase.api.core.JsonMissing +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.core.http.HttpResponse.Handler +import com.browserbase.api.errors.BadRequestException +import com.browserbase.api.errors.InternalServerException +import com.browserbase.api.errors.NotFoundException +import com.browserbase.api.errors.PermissionDeniedException +import com.browserbase.api.errors.RateLimitException +import com.browserbase.api.errors.UnauthorizedException +import com.browserbase.api.errors.UnexpectedStatusCodeException +import com.browserbase.api.errors.UnprocessableEntityException +import com.fasterxml.jackson.databind.json.JsonMapper + +@JvmSynthetic +internal fun errorBodyHandler(jsonMapper: JsonMapper): Handler { + val handler = jsonHandler(jsonMapper) + + return object : Handler { + override fun handle(response: HttpResponse): JsonValue = + try { + handler.handle(response) + } catch (e: Exception) { + JsonMissing.of() + } + } +} + +@JvmSynthetic +internal fun errorHandler(errorBodyHandler: Handler): Handler = + object : Handler { + override fun handle(response: HttpResponse): HttpResponse = + when (val statusCode = response.statusCode()) { + in 200..299 -> response + 400 -> + throw BadRequestException.builder() + .headers(response.headers()) + .body(errorBodyHandler.handle(response)) + .build() + 401 -> + throw UnauthorizedException.builder() + .headers(response.headers()) + .body(errorBodyHandler.handle(response)) + .build() + 403 -> + throw PermissionDeniedException.builder() + .headers(response.headers()) + .body(errorBodyHandler.handle(response)) + .build() + 404 -> + throw NotFoundException.builder() + .headers(response.headers()) + .body(errorBodyHandler.handle(response)) + .build() + 422 -> + throw UnprocessableEntityException.builder() + .headers(response.headers()) + .body(errorBodyHandler.handle(response)) + .build() + 429 -> + throw RateLimitException.builder() + .headers(response.headers()) + .body(errorBodyHandler.handle(response)) + .build() + in 500..599 -> + throw InternalServerException.builder() + .statusCode(statusCode) + .headers(response.headers()) + .body(errorBodyHandler.handle(response)) + .build() + else -> + throw UnexpectedStatusCodeException.builder() + .statusCode(statusCode) + .headers(response.headers()) + .body(errorBodyHandler.handle(response)) + .build() + } + } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/JsonHandler.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/JsonHandler.kt new file mode 100644 index 0000000..28b3462 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/JsonHandler.kt @@ -0,0 +1,20 @@ +@file:JvmName("JsonHandler") + +package com.browserbase.api.core.handlers + +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.core.http.HttpResponse.Handler +import com.browserbase.api.errors.StagehandInvalidDataException +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef + +@JvmSynthetic +internal inline fun jsonHandler(jsonMapper: JsonMapper): Handler = + object : Handler { + override fun handle(response: HttpResponse): T = + try { + jsonMapper.readValue(response.body(), jacksonTypeRef()) + } catch (e: Exception) { + throw StagehandInvalidDataException("Error reading response", e) + } + } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/SseHandler.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/SseHandler.kt new file mode 100644 index 0000000..d0ddde2 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/SseHandler.kt @@ -0,0 +1,125 @@ +// File generated from our OpenAPI spec by Stainless. + +@file:JvmName("SseHandler") + +package com.browserbase.api.core.handlers + +import com.browserbase.api.core.JsonMissing +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.core.http.HttpResponse.Handler +import com.browserbase.api.core.http.SseMessage +import com.browserbase.api.core.http.StreamResponse +import com.browserbase.api.core.http.map +import com.browserbase.api.errors.SseException +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef + +@JvmSynthetic +internal fun sseHandler(jsonMapper: JsonMapper): Handler> = + streamHandler { response, lines -> + val state = SseState(jsonMapper) + for (line in lines) { + val message = state.decode(line) ?: continue + + when (message.event) { + "error" -> { + throw SseException.builder() + .statusCode(response.statusCode()) + .headers(response.headers()) + .body( + try { + jsonMapper.readValue(message.data, jacksonTypeRef()) + } catch (e: Exception) { + JsonMissing.of() + } + ) + .build() + } + "starting", + "connected", + "running", + "finished" -> yield(message) + } + } + } + +private class SseState( + val jsonMapper: JsonMapper, + var event: String? = null, + val data: MutableList = mutableListOf(), + var lastId: String? = null, + var retry: Int? = null, +) { + // https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + fun decode(line: String): SseMessage? { + if (line.isEmpty()) { + return flush() + } + + if (line.startsWith(':')) { + return null + } + + val fieldName: String + var value: String + + val colonIndex = line.indexOf(':') + if (colonIndex == -1) { + fieldName = line + value = "" + } else { + fieldName = line.substring(0, colonIndex) + value = line.substring(colonIndex + 1) + } + + if (value.startsWith(' ')) { + value = value.substring(1) + } + + when (fieldName) { + "event" -> event = value + "data" -> data.add(value) + "id" -> { + if (!value.contains('\u0000')) { + lastId = value + } + } + "retry" -> value.toIntOrNull()?.let { retry = it } + } + + return null + } + + private fun flush(): SseMessage? { + if (isEmpty()) { + return null + } + + val message = + SseMessage.builder() + .jsonMapper(jsonMapper) + .event(event) + .data(data.joinToString("\n")) + .id(lastId) + .retry(retry) + .build() + + // NOTE: Per the SSE spec, do not reset lastId. + event = null + data.clear() + retry = null + + return message + } + + private fun isEmpty(): Boolean = + event.isNullOrEmpty() && data.isEmpty() && lastId.isNullOrEmpty() && retry == null +} + +@JvmSynthetic +internal inline fun Handler>.mapJson(): + Handler> = + object : Handler> { + override fun handle(response: HttpResponse): StreamResponse = + this@mapJson.handle(response).map { it.json() } + } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/StreamHandler.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/StreamHandler.kt new file mode 100644 index 0000000..4cd4f25 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/StreamHandler.kt @@ -0,0 +1,102 @@ +@file:JvmName("StreamHandler") + +package com.browserbase.api.core.handlers + +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.core.http.HttpResponse.Handler +import com.browserbase.api.core.http.PhantomReachableClosingStreamResponse +import com.browserbase.api.core.http.StreamResponse +import com.browserbase.api.errors.StagehandIoException +import java.io.IOException +import java.util.stream.Stream +import kotlin.streams.asStream + +@JvmSynthetic +internal fun streamHandler( + block: suspend SequenceScope.(response: HttpResponse, lines: Sequence) -> Unit +): Handler> = + object : Handler> { + + override fun handle(response: HttpResponse): StreamResponse { + val reader = response.body().bufferedReader() + val sequence = + // Wrap in a `CloseableSequence` to avoid performing a read on the `reader` + // after it has been closed, which would throw an `IOException`. + CloseableSequence( + sequence { + reader.useLines { lines -> + block( + response, + // We wrap the `lines` instead of the top-level sequence because + // we only want to catch `IOException` from the reader; not from + // the user's own code. + IOExceptionWrappingSequence(lines), + ) + } + } + .constrainOnce() + ) + + return PhantomReachableClosingStreamResponse( + object : StreamResponse { + + override fun stream(): Stream = sequence.asStream() + + override fun close() { + sequence.close() + reader.close() + response.close() + } + } + ) + } + } + +/** A sequence that catches, wraps, and rethrows [IOException] as [StagehandIoException]. */ +private class IOExceptionWrappingSequence(private val sequence: Sequence) : Sequence { + + override fun iterator(): Iterator { + val iterator = sequence.iterator() + return object : Iterator { + + override fun next(): T = + try { + iterator.next() + } catch (e: IOException) { + throw StagehandIoException("Stream failed", e) + } + + override fun hasNext(): Boolean = + try { + iterator.hasNext() + } catch (e: IOException) { + throw StagehandIoException("Stream failed", e) + } + } + } +} + +/** + * A sequence that can be closed. + * + * Once [close] is called, it will not yield more elements. It will also no longer consult the + * underlying [Iterator.hasNext] method. + */ +private class CloseableSequence(private val sequence: Sequence) : Sequence { + + private var isClosed: Boolean = false + + override fun iterator(): Iterator { + val iterator = sequence.iterator() + return object : Iterator { + + override fun next(): T = iterator.next() + + override fun hasNext(): Boolean = !isClosed && iterator.hasNext() + } + } + + fun close() { + isClosed = true + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/StringHandler.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/StringHandler.kt new file mode 100644 index 0000000..044de72 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/StringHandler.kt @@ -0,0 +1,13 @@ +@file:JvmName("StringHandler") + +package com.browserbase.api.core.handlers + +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.core.http.HttpResponse.Handler + +@JvmSynthetic internal fun stringHandler(): Handler = StringHandlerInternal + +private object StringHandlerInternal : Handler { + override fun handle(response: HttpResponse): String = + response.body().readBytes().toString(Charsets.UTF_8) +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/AsyncStreamResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/AsyncStreamResponse.kt new file mode 100644 index 0000000..ccc5d24 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/AsyncStreamResponse.kt @@ -0,0 +1,157 @@ +package com.browserbase.api.core.http + +import com.browserbase.api.core.http.AsyncStreamResponse.Handler +import java.util.Optional +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicReference + +/** + * A class providing access to an API response as an asynchronous stream of chunks of type [T], + * where each chunk can be individually processed as soon as it arrives instead of waiting on the + * full response. + */ +interface AsyncStreamResponse { + + /** + * Registers [handler] to be called for events of this stream. + * + * [handler]'s methods will be called in the client's configured or default thread pool. + * + * @throws IllegalStateException if [subscribe] has already been called. + */ + fun subscribe(handler: Handler): AsyncStreamResponse + + /** + * Registers [handler] to be called for events of this stream. + * + * [handler]'s methods will be called in the given [executor]. + * + * @throws IllegalStateException if [subscribe] has already been called. + */ + fun subscribe(handler: Handler, executor: Executor): AsyncStreamResponse + + /** + * Returns a future that completes when a stream is fully consumed, errors, or gets closed + * early. + */ + fun onCompleteFuture(): CompletableFuture + + /** + * Closes this resource, relinquishing any underlying resources. + * + * This is purposefully not inherited from [AutoCloseable] because this response should not be + * synchronously closed via try-with-resources. + */ + fun close() + + /** A class for handling streaming events. */ + fun interface Handler { + + /** Called whenever a chunk is received. */ + fun onNext(value: T) + + /** + * Called when a stream is fully consumed, errors, or gets closed early. + * + * [onNext] will not be called once this method is called. + * + * @param error Non-empty if the stream completed due to an error. + */ + fun onComplete(error: Optional) {} + } +} + +@JvmSynthetic +internal fun CompletableFuture>.toAsync(streamHandlerExecutor: Executor) = + PhantomReachableClosingAsyncStreamResponse( + object : AsyncStreamResponse { + + private val onCompleteFuture = CompletableFuture() + private val state = AtomicReference(State.NEW) + + init { + this@toAsync.whenComplete { _, error -> + // If an error occurs from the original future, then we should resolve the + // `onCompleteFuture` even if `subscribe` has not been called. + error?.let(onCompleteFuture::completeExceptionally) + } + } + + override fun subscribe(handler: Handler): AsyncStreamResponse = + subscribe(handler, streamHandlerExecutor) + + override fun subscribe( + handler: Handler, + executor: Executor, + ): AsyncStreamResponse = apply { + // TODO(JDK): Use `compareAndExchange` once targeting JDK 9. + check(state.compareAndSet(State.NEW, State.SUBSCRIBED)) { + if (state.get() == State.SUBSCRIBED) "Cannot subscribe more than once" + else "Cannot subscribe after the response is closed" + } + + this@toAsync.whenCompleteAsync( + { streamResponse, futureError -> + if (state.get() == State.CLOSED) { + // Avoid doing any work if `close` was called before the future + // completed. + return@whenCompleteAsync + } + + if (futureError != null) { + // An error occurred before we started passing chunks to the handler. + handler.onComplete(Optional.of(futureError)) + return@whenCompleteAsync + } + + var streamError: Throwable? = null + try { + streamResponse.stream().forEach(handler::onNext) + } catch (e: Throwable) { + streamError = e + } + + try { + handler.onComplete(Optional.ofNullable(streamError)) + } finally { + try { + // Notify completion via the `onCompleteFuture` as well. This is in + // a separate `try-finally` block so that we still complete the + // future if `handler.onComplete` throws. + if (streamError == null) { + onCompleteFuture.complete(null) + } else { + onCompleteFuture.completeExceptionally(streamError) + } + } finally { + close() + } + } + }, + executor, + ) + } + + override fun onCompleteFuture(): CompletableFuture = onCompleteFuture + + override fun close() { + val previousState = state.getAndSet(State.CLOSED) + if (previousState == State.CLOSED) { + return + } + + this@toAsync.whenComplete { streamResponse, error -> streamResponse?.close() } + // When the stream is closed, we should always consider it closed. If it closed due + // to an error, then we will have already completed the future earlier, and this + // will be a no-op. + onCompleteFuture.complete(null) + } + } + ) + +private enum class State { + NEW, + SUBSCRIBED, + CLOSED, +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/Headers.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/Headers.kt new file mode 100644 index 0000000..edb40c4 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/Headers.kt @@ -0,0 +1,115 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.http + +import com.browserbase.api.core.JsonArray +import com.browserbase.api.core.JsonBoolean +import com.browserbase.api.core.JsonMissing +import com.browserbase.api.core.JsonNull +import com.browserbase.api.core.JsonNumber +import com.browserbase.api.core.JsonObject +import com.browserbase.api.core.JsonString +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.toImmutable +import java.util.TreeMap + +class Headers +private constructor( + private val map: Map>, + @get:JvmName("size") val size: Int, +) { + + fun isEmpty(): Boolean = map.isEmpty() + + fun names(): Set = map.keys + + fun values(name: String): List = map[name].orEmpty() + + fun toBuilder(): Builder = Builder().putAll(map) + + companion object { + + @JvmStatic fun builder() = Builder() + } + + class Builder internal constructor() { + + private val map: MutableMap> = + TreeMap(String.CASE_INSENSITIVE_ORDER) + private var size: Int = 0 + + fun put(name: String, value: JsonValue): Builder = apply { + when (value) { + is JsonMissing, + is JsonNull -> {} + is JsonBoolean -> put(name, value.value.toString()) + is JsonNumber -> put(name, value.value.toString()) + is JsonString -> put(name, value.value) + is JsonArray -> value.values.forEach { put(name, it) } + is JsonObject -> + value.values.forEach { (nestedName, value) -> put("$name.$nestedName", value) } + } + } + + fun put(name: String, value: String) = apply { + map.getOrPut(name) { mutableListOf() }.add(value) + size++ + } + + fun put(name: String, values: Iterable) = apply { values.forEach { put(name, it) } } + + fun putAll(headers: Map>) = apply { headers.forEach(::put) } + + fun putAll(headers: Headers) = apply { + headers.names().forEach { put(it, headers.values(it)) } + } + + fun replace(name: String, value: String) = apply { + remove(name) + put(name, value) + } + + fun replace(name: String, values: Iterable) = apply { + remove(name) + put(name, values) + } + + fun replaceAll(headers: Map>) = apply { + headers.forEach(::replace) + } + + fun replaceAll(headers: Headers) = apply { + headers.names().forEach { replace(it, headers.values(it)) } + } + + fun remove(name: String) = apply { size -= map.remove(name).orEmpty().size } + + fun removeAll(names: Set) = apply { names.forEach(::remove) } + + fun clear() = apply { + map.clear() + size = 0 + } + + fun build() = + Headers( + map.mapValuesTo(TreeMap(String.CASE_INSENSITIVE_ORDER)) { (_, values) -> + values.toImmutable() + } + .toImmutable(), + size, + ) + } + + override fun hashCode(): Int = map.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Headers && map == other.map + } + + override fun toString(): String = "Headers{map=$map}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpClient.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpClient.kt new file mode 100644 index 0000000..d5a84ec --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpClient.kt @@ -0,0 +1,26 @@ +package com.browserbase.api.core.http + +import com.browserbase.api.core.RequestOptions +import java.lang.AutoCloseable +import java.util.concurrent.CompletableFuture + +interface HttpClient : AutoCloseable { + + fun execute( + request: HttpRequest, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponse + + fun execute(request: HttpRequest): HttpResponse = execute(request, RequestOptions.none()) + + fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + fun executeAsync(request: HttpRequest): CompletableFuture = + executeAsync(request, RequestOptions.none()) + + /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */ + override fun close() +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpMethod.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpMethod.kt new file mode 100644 index 0000000..fa2868d --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpMethod.kt @@ -0,0 +1,13 @@ +package com.browserbase.api.core.http + +enum class HttpMethod { + GET, + HEAD, + POST, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH, +} 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 new file mode 100644 index 0000000..280bd51 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequest.kt @@ -0,0 +1,176 @@ +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( + @get:JvmName("method") val method: HttpMethod, + @get:JvmName("baseUrl") val baseUrl: String, + @get:JvmName("pathSegments") val pathSegments: List, + @get:JvmName("headers") val headers: Headers, + @get:JvmName("queryParams") val queryParams: QueryParams, + @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 = + "HttpRequest{method=$method, baseUrl=$baseUrl, pathSegments=$pathSegments, headers=$headers, queryParams=$queryParams, body=$body}" + + companion object { + @JvmStatic fun builder() = Builder() + } + + class Builder internal constructor() { + + private var method: HttpMethod? = null + private var baseUrl: String? = null + private var pathSegments: MutableList = mutableListOf() + private var headers: Headers.Builder = Headers.builder() + private var queryParams: QueryParams.Builder = QueryParams.builder() + private var body: HttpRequestBody? = null + + @JvmSynthetic + internal fun from(request: HttpRequest) = apply { + method = request.method + baseUrl = request.baseUrl + pathSegments = request.pathSegments.toMutableList() + headers = request.headers.toBuilder() + queryParams = request.queryParams.toBuilder() + body = request.body + } + + fun method(method: HttpMethod) = apply { this.method = method } + + fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl } + + fun addPathSegment(pathSegment: String) = apply { pathSegments.add(pathSegment) } + + fun addPathSegments(vararg pathSegments: String) = apply { + this.pathSegments.addAll(pathSegments) + } + + fun headers(headers: Headers) = apply { + this.headers.clear() + putAllHeaders(headers) + } + + fun headers(headers: Map>) = apply { + this.headers.clear() + putAllHeaders(headers) + } + + fun putHeader(name: String, value: String) = apply { headers.put(name, value) } + + fun putHeaders(name: String, values: Iterable) = apply { headers.put(name, values) } + + fun putAllHeaders(headers: Headers) = apply { this.headers.putAll(headers) } + + fun putAllHeaders(headers: Map>) = apply { + this.headers.putAll(headers) + } + + fun replaceHeaders(name: String, value: String) = apply { headers.replace(name, value) } + + fun replaceHeaders(name: String, values: Iterable) = apply { + headers.replace(name, values) + } + + fun replaceAllHeaders(headers: Headers) = apply { this.headers.replaceAll(headers) } + + fun replaceAllHeaders(headers: Map>) = apply { + this.headers.replaceAll(headers) + } + + fun removeHeaders(name: String) = apply { headers.remove(name) } + + fun removeAllHeaders(names: Set) = apply { headers.removeAll(names) } + + fun queryParams(queryParams: QueryParams) = apply { + this.queryParams.clear() + putAllQueryParams(queryParams) + } + + fun queryParams(queryParams: Map>) = apply { + this.queryParams.clear() + putAllQueryParams(queryParams) + } + + fun putQueryParam(key: String, value: String) = apply { queryParams.put(key, value) } + + fun putQueryParams(key: String, values: Iterable) = apply { + queryParams.put(key, values) + } + + fun putAllQueryParams(queryParams: QueryParams) = apply { + this.queryParams.putAll(queryParams) + } + + fun putAllQueryParams(queryParams: Map>) = apply { + this.queryParams.putAll(queryParams) + } + + fun replaceQueryParams(key: String, value: String) = apply { + queryParams.replace(key, value) + } + + fun replaceQueryParams(key: String, values: Iterable) = apply { + queryParams.replace(key, values) + } + + fun replaceAllQueryParams(queryParams: QueryParams) = apply { + this.queryParams.replaceAll(queryParams) + } + + fun replaceAllQueryParams(queryParams: Map>) = apply { + this.queryParams.replaceAll(queryParams) + } + + fun removeQueryParams(key: String) = apply { queryParams.remove(key) } + + fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) } + + fun body(body: HttpRequestBody) = apply { this.body = body } + + fun build(): HttpRequest = + HttpRequest( + checkRequired("method", method), + checkRequired("baseUrl", baseUrl), + pathSegments.toImmutable(), + headers.build(), + queryParams.build(), + body, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequestBodies.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequestBodies.kt new file mode 100644 index 0000000..75a778b --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequestBodies.kt @@ -0,0 +1,267 @@ +// File generated from our OpenAPI spec by Stainless. + +@file:JvmName("HttpRequestBodies") + +package com.browserbase.api.core.http + +import com.browserbase.api.core.MultipartField +import com.browserbase.api.core.toImmutable +import com.browserbase.api.errors.StagehandInvalidDataException +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.databind.node.JsonNodeType +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.io.OutputStream +import java.util.UUID +import kotlin.jvm.optionals.getOrNull + +@JvmSynthetic +internal inline fun json(jsonMapper: JsonMapper, value: T): HttpRequestBody = + object : HttpRequestBody { + private val bytes: ByteArray by lazy { jsonMapper.writeValueAsBytes(value) } + + override fun writeTo(outputStream: OutputStream) = outputStream.write(bytes) + + override fun contentType(): String = "application/json" + + override fun contentLength(): Long = bytes.size.toLong() + + override fun repeatable(): Boolean = true + + override fun close() {} + } + +@JvmSynthetic +internal fun multipartFormData( + jsonMapper: JsonMapper, + fields: Map>, +): HttpRequestBody = + MultipartBody.Builder() + .apply { + fields.forEach { (name, field) -> + val knownValue = field.value.asKnown().getOrNull() + val parts = + if (knownValue is InputStream) { + // Read directly from the `InputStream` instead of reading it all + // into memory due to the `jsonMapper` serialization below. + sequenceOf(name to knownValue) + } else { + val node = jsonMapper.valueToTree(field.value) + serializePart(name, node) + } + + parts.forEach { (name, bytes) -> + val partBody = + if (bytes is ByteArrayInputStream) { + val byteArray = bytes.readBytes() + + object : HttpRequestBody { + + override fun writeTo(outputStream: OutputStream) { + outputStream.write(byteArray) + } + + override fun contentType(): String = field.contentType + + override fun contentLength(): Long = byteArray.size.toLong() + + override fun repeatable(): Boolean = true + + override fun close() {} + } + } else { + object : HttpRequestBody { + + override fun writeTo(outputStream: OutputStream) { + bytes.copyTo(outputStream) + } + + override fun contentType(): String = field.contentType + + override fun contentLength(): Long = -1L + + override fun repeatable(): Boolean = false + + override fun close() = bytes.close() + } + } + + addPart( + MultipartBody.Part.create( + name, + field.filename().getOrNull(), + field.contentType, + partBody, + ) + ) + } + } + } + .build() + +private fun serializePart(name: String, node: JsonNode): Sequence> = + when (node.nodeType) { + JsonNodeType.MISSING, + JsonNodeType.NULL -> emptySequence() + JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream()) + JsonNodeType.STRING -> sequenceOf(name to node.textValue().byteInputStream()) + JsonNodeType.BOOLEAN -> sequenceOf(name to node.booleanValue().toString().byteInputStream()) + JsonNodeType.NUMBER -> sequenceOf(name to node.numberValue().toString().byteInputStream()) + JsonNodeType.ARRAY -> + sequenceOf( + name to + node + .elements() + .asSequence() + .mapNotNull { element -> + when (element.nodeType) { + JsonNodeType.MISSING, + JsonNodeType.NULL -> null + JsonNodeType.STRING -> element.textValue() + JsonNodeType.BOOLEAN -> element.booleanValue().toString() + JsonNodeType.NUMBER -> element.numberValue().toString() + null, + JsonNodeType.BINARY, + JsonNodeType.ARRAY, + JsonNodeType.OBJECT, + JsonNodeType.POJO -> + throw StagehandInvalidDataException( + "Unexpected JsonNode type in array: ${element.nodeType}" + ) + } + } + .joinToString(",") + .byteInputStream() + ) + JsonNodeType.OBJECT -> + node.fields().asSequence().flatMap { (key, value) -> + serializePart("$name[$key]", value) + } + JsonNodeType.POJO, + null -> throw StagehandInvalidDataException("Unexpected JsonNode type: ${node.nodeType}") + } + +private class MultipartBody +private constructor(private val boundary: String, private val parts: List) : HttpRequestBody { + private val boundaryBytes: ByteArray = boundary.toByteArray() + private val contentType = "multipart/form-data; boundary=$boundary" + + // This must remain in sync with `contentLength`. + override fun writeTo(outputStream: OutputStream) { + parts.forEach { part -> + outputStream.write(DASHDASH) + outputStream.write(boundaryBytes) + outputStream.write(CRLF) + + outputStream.write(CONTENT_DISPOSITION) + outputStream.write(part.contentDisposition.toByteArray()) + outputStream.write(CRLF) + + outputStream.write(CONTENT_TYPE) + outputStream.write(part.contentType.toByteArray()) + outputStream.write(CRLF) + + outputStream.write(CRLF) + part.body.writeTo(outputStream) + outputStream.write(CRLF) + } + + outputStream.write(DASHDASH) + outputStream.write(boundaryBytes) + outputStream.write(DASHDASH) + outputStream.write(CRLF) + } + + override fun contentType(): String = contentType + + // This must remain in sync with `writeTo`. + override fun contentLength(): Long { + var byteCount = 0L + + parts.forEach { part -> + val contentLength = part.body.contentLength() + if (contentLength == -1L) { + return -1L + } + + byteCount += + DASHDASH.size + + boundaryBytes.size + + CRLF.size + + CONTENT_DISPOSITION.size + + part.contentDisposition.toByteArray().size + + CRLF.size + + CONTENT_TYPE.size + + part.contentType.toByteArray().size + + CRLF.size + + CRLF.size + + contentLength + + CRLF.size + } + + byteCount += DASHDASH.size + boundaryBytes.size + DASHDASH.size + CRLF.size + return byteCount + } + + override fun repeatable(): Boolean = parts.all { it.body.repeatable() } + + override fun close() { + parts.forEach { it.body.close() } + } + + class Builder { + private val boundary = UUID.randomUUID().toString() + private val parts: MutableList = mutableListOf() + + fun addPart(part: Part) = apply { parts.add(part) } + + fun build() = MultipartBody(boundary, parts.toImmutable()) + } + + class Part + private constructor( + val contentDisposition: String, + val contentType: String, + val body: HttpRequestBody, + ) { + companion object { + fun create( + name: String, + filename: String?, + contentType: String, + body: HttpRequestBody, + ): Part { + val disposition = buildString { + append("form-data; name=") + appendQuotedString(name) + if (filename != null) { + append("; filename=") + appendQuotedString(filename) + } + } + return Part(disposition, contentType, body) + } + } + } + + companion object { + private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte()) + private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte()) + private val CONTENT_DISPOSITION = "Content-Disposition: ".toByteArray() + private val CONTENT_TYPE = "Content-Type: ".toByteArray() + + private fun StringBuilder.appendQuotedString(key: String) { + append('"') + for (ch in key) { + when (ch) { + '\n' -> append("%0A") + '\r' -> append("%0D") + '"' -> append("%22") + else -> append(ch) + } + } + append('"') + } + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequestBody.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequestBody.kt new file mode 100644 index 0000000..57c20f4 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequestBody.kt @@ -0,0 +1,25 @@ +package com.browserbase.api.core.http + +import java.io.OutputStream +import java.lang.AutoCloseable + +interface HttpRequestBody : AutoCloseable { + + fun writeTo(outputStream: OutputStream) + + fun contentType(): String? + + fun contentLength(): Long + + /** + * Determines if a request can be repeated in a meaningful way, for example before doing a + * retry. + * + * The most typical case when a request can't be retried is if the request body is being + * streamed. In this case the body data isn't available on subsequent attempts. + */ + fun repeatable(): Boolean + + /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */ + override fun close() +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpResponse.kt new file mode 100644 index 0000000..045efac --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpResponse.kt @@ -0,0 +1,22 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.http + +import java.io.InputStream + +interface HttpResponse : AutoCloseable { + + fun statusCode(): Int + + fun headers(): Headers + + fun body(): InputStream + + /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */ + override fun close() + + interface Handler { + + fun handle(response: HttpResponse): T + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpResponseFor.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpResponseFor.kt new file mode 100644 index 0000000..91ba762 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpResponseFor.kt @@ -0,0 +1,25 @@ +package com.browserbase.api.core.http + +import java.io.InputStream + +interface HttpResponseFor : HttpResponse { + + fun parse(): T +} + +@JvmSynthetic +internal fun HttpResponse.parseable(parse: () -> T): HttpResponseFor = + object : HttpResponseFor { + + private val parsed: T by lazy { parse() } + + override fun parse(): T = parsed + + override fun statusCode(): Int = this@parseable.statusCode() + + override fun headers(): Headers = this@parseable.headers() + + override fun body(): InputStream = this@parseable.body() + + override fun close() = this@parseable.close() + } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/LoggingHttpClient.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/LoggingHttpClient.kt new file mode 100644 index 0000000..564bbfc --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/LoggingHttpClient.kt @@ -0,0 +1,639 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.http + +import com.browserbase.api.core.LogLevel +import com.browserbase.api.core.RequestOptions +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.toImmutable +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.charset.CharacterCodingException +import java.nio.charset.Charset +import java.nio.charset.CharsetDecoder +import java.nio.charset.CodingErrorAction +import java.nio.charset.StandardCharsets +import java.time.Clock +import java.time.Duration +import java.time.OffsetDateTime +import java.util.SortedSet +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException +import kotlin.time.toKotlinDuration + +/** A wrapper [HttpClient] around [httpClient] that logs request and response information. */ +class LoggingHttpClient +private constructor( + /** The underlying [HttpClient] for making requests. */ + @get:JvmName("httpClient") val httpClient: HttpClient, + /** + * Sensitive headers to redact from logs. + * + * Defaults to `Set.of("authorization", "api-key", "x-api-key", "cookie", "set-cookie", + * "x-bb-api-key", "x-bb-project-id", "x-model-api-key")`. + */ + @get:JvmName("redactedHeaders") val redactedHeaders: SortedSet, + /** + * The clock to use for measuring request and response durations. + * + * This is primarily useful for using a fake clock in tests. + * + * Defaults to [Clock.systemUTC]. + */ + @get:JvmName("clock") val clock: Clock, + /** + * The log level to use. + * + * Pass [LogLevel.fromEnv] to read from environment variables. + */ + @get:JvmName("level") val level: LogLevel, +) : HttpClient { + + override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse { + val loggingRequest = logRequest(request) + + val before = OffsetDateTime.now(clock) + val response = + try { + httpClient.execute(loggingRequest, requestOptions) + } catch (e: Throwable) { + logFailure(e, Duration.between(before, OffsetDateTime.now(clock))) + throw e + } + + val took = Duration.between(before, OffsetDateTime.now(clock)) + return logResponse(response, took) + } + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture { + val loggingRequest = logRequest(request) + + val before = OffsetDateTime.now(clock) + val future = + try { + httpClient.executeAsync(loggingRequest, requestOptions) + } catch (e: Throwable) { + logFailure(e, Duration.between(before, OffsetDateTime.now(clock))) + throw e + } + return future.handle { response, error -> + val took = Duration.between(before, OffsetDateTime.now(clock)) + if (error != null) { + logFailure(unwrapCompletionException(error), took) + throw error + } + logResponse(response, took) + } + } + + private fun logRequest(request: HttpRequest): HttpRequest { + if (!level.shouldLog(LogLevel.INFO)) { + return request + } + + System.err.println( + buildString { + append("--> ${request.method} ${request.url()}") + request.body?.let { + val length = it.contentLength() + append(if (length >= 0) " ($length-byte body)" else " (unknown-length body)") + } + } + ) + + if (!level.shouldLog(LogLevel.DEBUG)) { + return request + } + + logHeaders(request.headers) + + if (request.body == null) { + System.err.println("--> END ${request.method}") + System.err.println() + return request + } + + return request + .toBuilder() + .body(LoggingHttpRequestBody(request.method, request.body)) + .build() + } + + private fun logResponse(response: HttpResponse, took: Duration): HttpResponse { + if (!level.shouldLog(LogLevel.INFO)) { + return response + } + + val contentLength = response.headers().values("Content-Length").firstOrNull()?.toIntOrNull() + System.err.println( + "<-- ${response.statusCode()} (${ + buildString { + append(took.format()) + contentLength?.let { append(", $contentLength-byte body") } + } + })" + ) + + if (!level.shouldLog(LogLevel.DEBUG)) { + return response + } + + logHeaders(response.headers()) + return LoggingHttpResponse(response) + } + + private fun logFailure(error: Throwable, took: Duration) { + if (!level.shouldLog(LogLevel.ERROR)) { + return + } + + System.err.println( + buildString { + append("<-- !! ${error.javaClass.simpleName}") + error.message?.let { append(": $it") } + append(" (${took.format()})") + } + ) + } + + private fun unwrapCompletionException(error: Throwable): Throwable = + if (error is CompletionException && error.cause != null) error.cause!! else error + + private fun logHeaders(headers: Headers) = + headers.names().forEach { name -> + headers.values(name).forEach { value -> + System.err.println("$name: ${if (redactedHeaders.contains(name)) "██" else value}") + } + } + + override fun close() = httpClient.close() + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [LoggingHttpClient]. + * + * The following fields are required: + * ```java + * .httpClient() + * .level() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [LoggingHttpClient]. */ + class Builder internal constructor() { + + private var httpClient: HttpClient? = null + private var redactedHeaders: Set = + setOf( + "authorization", + "api-key", + "x-api-key", + "cookie", + "set-cookie", + "x-bb-api-key", + "x-bb-project-id", + "x-model-api-key", + ) + private var clock: Clock = Clock.systemUTC() + private var level: LogLevel? = null + + @JvmSynthetic + internal fun from(loggingHttpClient: LoggingHttpClient) = apply { + httpClient = loggingHttpClient.httpClient + redactedHeaders = loggingHttpClient.redactedHeaders + clock = loggingHttpClient.clock + level = loggingHttpClient.level + } + + /** The underlying [HttpClient] for making requests. */ + fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient } + + /** + * Sensitive headers to redact from logs. + * + * Defaults to `Set.of("authorization", "api-key", "x-api-key", "cookie", "set-cookie", + * "x-bb-api-key", "x-bb-project-id", "x-model-api-key")`. + */ + fun redactedHeaders(redactedHeaders: Set) = apply { + this.redactedHeaders = redactedHeaders + } + + /** + * The clock to use for measuring request and response durations. + * + * This is primarily useful for using a fake clock in tests. + * + * Defaults to [Clock.systemUTC]. + */ + fun clock(clock: Clock) = apply { this.clock = clock } + + /** + * The log level to use. + * + * Pass [LogLevel.fromEnv] to read from environment variables. + */ + fun level(level: LogLevel) = apply { this.level = level } + + /** + * Returns an immutable instance of [LoggingHttpClient]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .httpClient() + * .level() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): LoggingHttpClient = + LoggingHttpClient( + checkRequired("httpClient", httpClient), + redactedHeaders.toSortedSet(String.CASE_INSENSITIVE_ORDER).toImmutable(), + clock, + checkRequired("level", level), + ) + } +} + +/** + * An [HttpRequestBody] wrapper that delegates to [body] while also logging line by line as it's + * written. + * + * The logging occurs in a streaming manner with minimal buffering. + */ +private class LoggingHttpRequestBody( + private val method: HttpMethod, + private val body: HttpRequestBody, +) : HttpRequestBody { + + private val charset by lazy { parseCharset(body.contentType()) } + + override fun writeTo(outputStream: OutputStream) { + val loggingOutputStream = LoggingOutputStream(outputStream, charset) + body.writeTo(loggingOutputStream) + + loggingOutputStream.flush() + System.err.println("--> END $method (${loggingOutputStream.writeCount()}-byte body)") + System.err.println() + } + + override fun contentType(): String? = body.contentType() + + override fun contentLength(): Long = body.contentLength() + + override fun repeatable(): Boolean = body.repeatable() + + override fun close() = body.close() +} + +/** + * An [OutputStream] wrapper that delegates to [outputStream] while also logging bytes line by line + * as it's written to. + * + * The written content is assumed to be in the given [charset] and the logging occurs in a streaming + * manner with minimal buffering. + */ +private class LoggingOutputStream(private val outputStream: OutputStream, charset: Charset?) : + OutputStream() { + + private val buffer = LoggingBuffer(charset) + + fun writeCount() = buffer.writeCount() + + override fun write(b: Int) { + outputStream.write(b) + buffer.write(b) + } + + override fun write(b: ByteArray, off: Int, len: Int) { + outputStream.write(b, off, len) + for (i in off until off + len) { + buffer.write(b[i].toInt() and 0xFF) + } + } + + /** Prints any currently buffered content. */ + override fun flush() { + buffer.flush() + outputStream.flush() + } + + override fun close() = outputStream.close() +} + +/** + * An [HttpResponse] wrapper that delegates to [response] while also logging line-by-line as it's + * read. + * + * The logging occurs in a streaming manner with minimal buffering. + */ +private class LoggingHttpResponse(private val response: HttpResponse) : HttpResponse { + + private val loggingBody: Lazy = lazy { + LoggingInputStream( + response.body(), + parseCharset(response.headers().values("Content-Type").firstOrNull()), + ) + } + + override fun statusCode(): Int = response.statusCode() + + override fun headers(): Headers = response.headers() + + override fun body(): InputStream = loggingBody.value + + override fun close() { + if (loggingBody.isInitialized()) { + loggingBody.value.close() + } + response.close() + } +} + +/** + * An [InputStream] wrapper that delegates to [inputStream] while also logging bytes line by line as + * it's read. + * + * The contents of [inputStream] are assumed to be in the given [charset] and the logging occurs in + * a streaming manner with minimal buffering. + */ +private class LoggingInputStream(private val inputStream: InputStream, charset: Charset?) : + InputStream() { + + private var isDone = false + private val buffer = LoggingBuffer(charset) + + override fun read(): Int { + if (isDone) { + return -1 + } + + val b = inputStream.read() + + if (b == -1) { + markDone() + return b + } + + buffer.write(b) + return b + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + if (isDone) { + return -1 + } + + val bytesRead = inputStream.read(b, off, len) + + if (bytesRead == -1) { + markDone() + return bytesRead + } + + for (i in off until off + bytesRead) { + buffer.write(b[i].toInt() and 0xFF) + } + return bytesRead + } + + override fun close() { + if (!isDone) { + markDone(closedEarly = true) + } + inputStream.close() + } + + private fun markDone(closedEarly: Boolean = false) { + isDone = true + buffer.flush() + val suffix = if (closedEarly) ", closed early" else "" + System.err.println("<-- END HTTP (${buffer.writeCount()}-byte body$suffix)") + System.err.println() + } +} + +/** + * A byte buffer that prints line by line, using the given [charset], as bytes are written to it. + * + * When [charset] is `null`, the buffer performs an upfront check to detect binary content. If + * non-whitespace ISO control characters are found in the first [PROBABLY_UTF8_CODE_POINT_LIMIT] + * code points, body logging is suppressed entirely. + */ +private class LoggingBuffer(charset: Charset?) { + + private val charset = charset ?: StandardCharsets.UTF_8 + + private val decoder: CharsetDecoder = + this.charset + .newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + private var writeCount = 0 + private val buffer = ByteArrayOutputStream(128) + + /** + * Whether logging has been suppressed because the content doesn't appear to be readable text. + * + * This is only set when [charset] is `null` and the content fails the [isProbablyUtf8] check. + */ + private var suppressed = false + + /** + * Bytes accumulated for the [isProbablyUtf8] check before any lines are printed. + * + * Once the check passes (or [charset] is non-null), this is set to `null` and bytes flow + * directly to [buffer]. + */ + private var prefetchBuffer: ByteArrayOutputStream? = + if (charset != null) null else ByteArrayOutputStream(128) + + fun writeCount() = writeCount + + fun write(b: Int) { + if (writeCount == 0) { + // Print a newline before we start printing anything to separate the printed content + // from previous content. + System.err.println() + } + + writeCount++ + + if (suppressed) { + return + } + + val prefetch = prefetchBuffer + if (prefetch != null) { + prefetch.write(b) + // Continue accumulating until we have enough bytes to decide. + if (prefetch.size() < PROBABLY_UTF8_BYTE_LIMIT && b != '\n'.code) { + return + } + // We have enough bytes. Check if the content is probably UTF-8. + prefetchBuffer = null + val bytes = prefetch.toByteArray() + if (!isProbablyUtf8(bytes)) { + suppressed = true + System.err.println("(binary body omitted)") + return + } + // Content looks like UTF-8. Feed the accumulated bytes into the normal buffer. + for (byte in bytes) { + writeToBuffer(byte.toInt() and 0xFF) + } + return + } + + writeToBuffer(b) + } + + private fun writeToBuffer(b: Int) { + if (b == '\n'.code) { + flush() + return + } + + buffer.write(b) + } + + /** Prints any currently buffered content. */ + fun flush() { + if (suppressed) { + return + } + + // If we still have a prefetch buffer when flush is called (body was shorter than the + // limit), run the check now. + val prefetch = prefetchBuffer + if (prefetch != null) { + prefetchBuffer = null + val bytes = prefetch.toByteArray() + if (bytes.isEmpty()) { + return + } + if (!isProbablyUtf8(bytes)) { + suppressed = true + System.err.println("(binary body omitted)") + return + } + for (byte in bytes) { + writeToBuffer(byte.toInt() and 0xFF) + } + } + + if (buffer.size() == 0) { + return + } + + val line = + try { + decoder.decode(ByteBuffer.wrap(buffer.toByteArray())) + } catch (e: CharacterCodingException) { + "(omitted line is not valid $charset)" + } + buffer.reset() + System.err.println(line) + } +} + +/** The maximum number of code points to sample when checking if content is probably UTF-8. */ +private const val PROBABLY_UTF8_CODE_POINT_LIMIT = 64 + +/** + * The maximum number of bytes to accumulate before running the [isProbablyUtf8] check. UTF-8 code + * points are at most 4 bytes, so this accommodates [PROBABLY_UTF8_CODE_POINT_LIMIT] code points. + */ +private const val PROBABLY_UTF8_BYTE_LIMIT = PROBABLY_UTF8_CODE_POINT_LIMIT * 4 + +/** + * Returns `true` if the given [bytes] probably contain human-readable UTF-8 text. + * + * Decodes up to [PROBABLY_UTF8_CODE_POINT_LIMIT] code points and returns `false` if any + * non-whitespace ISO control characters are found, or if the bytes are not valid UTF-8. + */ +private fun isProbablyUtf8(bytes: ByteArray): Boolean { + try { + val decoder = + StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + val charBuffer = decoder.decode(ByteBuffer.wrap(bytes)) + var codePointCount = 0 + var i = 0 + while (i < charBuffer.length && codePointCount < PROBABLY_UTF8_CODE_POINT_LIMIT) { + val codePoint = Character.codePointAt(charBuffer, i) + if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { + return false + } + i += Character.charCount(codePoint) + codePointCount++ + } + return true + } catch (e: CharacterCodingException) { + return false + } +} + +/** Returns the [Charset] in the given [contentType] string, or `null` if unspecified. */ +private fun parseCharset(contentType: String?): Charset? = + contentType + ?.split(";") + ?.drop(1) + ?.map { it.trim() } + ?.firstOrNull { it.startsWith("charset=", ignoreCase = true) } + ?.substringAfter("=") + ?.trim() + ?.removeSurrounding("\"") + ?.let { runCatching { charset(it) }.getOrNull() } + +/** Formats the [Duration] into a string like "1m 40s 467ms". */ +private fun Duration.format(): String = + toKotlinDuration().toComponents { days, hours, minutes, seconds, nanoseconds -> + buildString { + val milliseconds = nanoseconds / 1_000_000 + if (days > 0) { + append("${days}d") + } + if (hours > 0) { + if (isNotEmpty()) { + append(" ") + } + append("${hours}h") + } + if (minutes > 0) { + if (isNotEmpty()) { + append(" ") + } + append("${minutes}m") + } + if (seconds > 0) { + if (isNotEmpty()) { + append(" ") + } + append("${seconds}s") + } + if (milliseconds > 0) { + if (isNotEmpty()) { + append(" ") + } + append("${milliseconds}ms") + } + + if (isEmpty()) { + append("0s") + } + } + } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/PhantomReachableClosingAsyncStreamResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/PhantomReachableClosingAsyncStreamResponse.kt new file mode 100644 index 0000000..13f1198 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/PhantomReachableClosingAsyncStreamResponse.kt @@ -0,0 +1,56 @@ +package com.browserbase.api.core.http + +import com.browserbase.api.core.closeWhenPhantomReachable +import com.browserbase.api.core.http.AsyncStreamResponse.Handler +import java.util.Optional +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor + +/** + * A delegating wrapper around an `AsyncStreamResponse` that closes it once it's only phantom + * reachable. + * + * This class ensures the `AsyncStreamResponse` is closed even if the user forgets to close it. + */ +internal class PhantomReachableClosingAsyncStreamResponse( + private val asyncStreamResponse: AsyncStreamResponse +) : AsyncStreamResponse { + + /** + * An object used for keeping `asyncStreamResponse` open while the object is still reachable. + */ + private val reachabilityTracker = Object() + + init { + closeWhenPhantomReachable(reachabilityTracker, asyncStreamResponse::close) + } + + override fun subscribe(handler: Handler): AsyncStreamResponse = apply { + asyncStreamResponse.subscribe(TrackedHandler(handler, reachabilityTracker)) + } + + override fun subscribe(handler: Handler, executor: Executor): AsyncStreamResponse = + apply { + asyncStreamResponse.subscribe(TrackedHandler(handler, reachabilityTracker), executor) + } + + override fun onCompleteFuture(): CompletableFuture = + asyncStreamResponse.onCompleteFuture() + + override fun close() = asyncStreamResponse.close() +} + +/** + * A wrapper around a `Handler` that also references a `reachabilityTracker` object. + * + * Referencing the `reachabilityTracker` object prevents it from getting reclaimed while the handler + * is still reachable. + */ +private class TrackedHandler( + private val handler: Handler, + private val reachabilityTracker: Any, +) : Handler { + override fun onNext(value: T) = handler.onNext(value) + + override fun onComplete(error: Optional) = handler.onComplete(error) +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/PhantomReachableClosingHttpClient.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/PhantomReachableClosingHttpClient.kt new file mode 100644 index 0000000..54e1c00 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/PhantomReachableClosingHttpClient.kt @@ -0,0 +1,26 @@ +package com.browserbase.api.core.http + +import com.browserbase.api.core.RequestOptions +import com.browserbase.api.core.closeWhenPhantomReachable +import java.util.concurrent.CompletableFuture + +/** + * A delegating wrapper around an `HttpClient` that closes it once it's only phantom reachable. + * + * This class ensures the `HttpClient` is closed even if the user forgets to close it. + */ +internal class PhantomReachableClosingHttpClient(private val httpClient: HttpClient) : HttpClient { + init { + closeWhenPhantomReachable(this, httpClient) + } + + override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse = + httpClient.execute(request, requestOptions) + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture = httpClient.executeAsync(request, requestOptions) + + override fun close() = httpClient.close() +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/PhantomReachableClosingStreamResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/PhantomReachableClosingStreamResponse.kt new file mode 100644 index 0000000..b977aab --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/PhantomReachableClosingStreamResponse.kt @@ -0,0 +1,21 @@ +package com.browserbase.api.core.http + +import com.browserbase.api.core.closeWhenPhantomReachable +import java.util.stream.Stream + +/** + * A delegating wrapper around a `StreamResponse` that closes it once it's only phantom reachable. + * + * This class ensures the `StreamResponse` is closed even if the user forgets to close it. + */ +internal class PhantomReachableClosingStreamResponse( + private val streamResponse: StreamResponse +) : StreamResponse { + init { + closeWhenPhantomReachable(this, streamResponse) + } + + override fun stream(): Stream = streamResponse.stream() + + override fun close() = streamResponse.close() +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/ProxyAuthenticator.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/ProxyAuthenticator.kt new file mode 100644 index 0000000..75e750b --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/ProxyAuthenticator.kt @@ -0,0 +1,59 @@ +package com.browserbase.api.core.http + +import java.net.Proxy +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.Base64 +import java.util.Optional + +/** + * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication Required`. + * + * Implementations inspect the 407 [response] (typically its `Proxy-Authenticate` header) and return + * the request to retry with a `Proxy-Authorization` header set, or [Optional.empty] to abandon + * authentication and surface the 407 to the caller. + * + * Implementations must be thread-safe; they may be invoked concurrently from multiple HTTP calls. + */ +fun interface ProxyAuthenticator { + + /** + * @param proxy the proxy that produced the challenge, or [Proxy.NO_PROXY] if the route is not + * yet established + * @param request the request that produced [response] + * @param response the 407 challenge response + * @return the retry request to send (typically [request] with a `Proxy-Authorization` header + * added), or [Optional.empty] to abandon authentication + */ + fun authenticate( + proxy: Proxy, + request: HttpRequest, + response: HttpResponse, + ): Optional + + companion object { + + /** + * A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the ISO-8859-1 + * charset. + */ + @JvmStatic + fun basic(username: String, password: String): ProxyAuthenticator = + basic(username, password, StandardCharsets.ISO_8859_1) + + /** + * A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the given [charset]. + */ + @JvmStatic + fun basic(username: String, password: String, charset: Charset): ProxyAuthenticator { + val token = + Base64.getEncoder().encodeToString("$username:$password".toByteArray(charset)) + val headerValue = "Basic $token" + return ProxyAuthenticator { _, request, _ -> + Optional.of( + request.toBuilder().putHeader("Proxy-Authorization", headerValue).build() + ) + } + } + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/QueryParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/QueryParams.kt new file mode 100644 index 0000000..e394ad0 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/QueryParams.kt @@ -0,0 +1,129 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.http + +import com.browserbase.api.core.JsonArray +import com.browserbase.api.core.JsonBoolean +import com.browserbase.api.core.JsonMissing +import com.browserbase.api.core.JsonNull +import com.browserbase.api.core.JsonNumber +import com.browserbase.api.core.JsonObject +import com.browserbase.api.core.JsonString +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.toImmutable + +class QueryParams +private constructor( + private val map: Map>, + @get:JvmName("size") val size: Int, +) { + + fun isEmpty(): Boolean = map.isEmpty() + + fun keys(): Set = map.keys + + fun values(key: String): List = map[key].orEmpty() + + fun toBuilder(): Builder = Builder().putAll(map) + + companion object { + + @JvmStatic fun builder() = Builder() + } + + class Builder internal constructor() { + + private val map: MutableMap> = mutableMapOf() + private var size: Int = 0 + + fun put(key: String, value: JsonValue): Builder = apply { + when (value) { + is JsonMissing, + is JsonNull -> {} + is JsonBoolean -> put(key, value.value.toString()) + is JsonNumber -> put(key, value.value.toString()) + is JsonString -> put(key, value.value) + is JsonArray -> + put( + key, + value.values + .asSequence() + .mapNotNull { + when (it) { + is JsonMissing, + is JsonNull -> null + is JsonBoolean -> it.value.toString() + is JsonNumber -> it.value.toString() + is JsonString -> it.value + is JsonArray, + is JsonObject -> + throw IllegalArgumentException( + "Cannot comma separate non-primitives in query params" + ) + } + } + .joinToString(","), + ) + is JsonObject -> + value.values.forEach { (nestedKey, value) -> put("$key[$nestedKey]", value) } + } + } + + fun put(key: String, value: String) = apply { + map.getOrPut(key) { mutableListOf() }.add(value) + size++ + } + + fun put(key: String, values: Iterable) = apply { values.forEach { put(key, it) } } + + fun putAll(queryParams: Map>) = apply { + queryParams.forEach(::put) + } + + fun putAll(queryParams: QueryParams) = apply { + queryParams.keys().forEach { put(it, queryParams.values(it)) } + } + + fun replace(key: String, value: String) = apply { + remove(key) + put(key, value) + } + + fun replace(key: String, values: Iterable) = apply { + remove(key) + put(key, values) + } + + fun replaceAll(queryParams: Map>) = apply { + queryParams.forEach(::replace) + } + + fun replaceAll(queryParams: QueryParams) = apply { + queryParams.keys().forEach { replace(it, queryParams.values(it)) } + } + + fun remove(key: String) = apply { size -= map.remove(key).orEmpty().size } + + fun removeAll(keys: Set) = apply { keys.forEach(::remove) } + + fun clear() = apply { + map.clear() + size = 0 + } + + fun build() = + QueryParams(map.mapValues { (_, values) -> values.toImmutable() }.toImmutable(), size) + } + + override fun hashCode(): Int = map.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is QueryParams && map == other.map + } + + override fun toString(): String = "QueryParams{map=$map}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/RetryingHttpClient.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/RetryingHttpClient.kt new file mode 100644 index 0000000..381b36f --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/RetryingHttpClient.kt @@ -0,0 +1,262 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.http + +import com.browserbase.api.core.DefaultSleeper +import com.browserbase.api.core.RequestOptions +import com.browserbase.api.core.Sleeper +import com.browserbase.api.core.checkRequired +import com.browserbase.api.errors.StagehandIoException +import com.browserbase.api.errors.StagehandRetryableException +import java.io.IOException +import java.time.Clock +import java.time.Duration +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException +import java.time.temporal.ChronoUnit +import java.util.UUID +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ThreadLocalRandom +import java.util.concurrent.TimeUnit +import java.util.function.Function +import kotlin.math.min +import kotlin.math.pow + +class RetryingHttpClient +private constructor( + private val httpClient: HttpClient, + private val sleeper: Sleeper, + private val clock: Clock, + private val maxRetries: Int, + private val idempotencyHeader: String?, +) : HttpClient { + + override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse { + var modifiedRequest = maybeAddIdempotencyHeader(request) + + // Don't send the current retry count in the headers if the caller set their own value. + val shouldSendRetryCount = + !modifiedRequest.headers.names().contains("X-Stainless-Retry-Count") + + var retries = 0 + + while (true) { + if (shouldSendRetryCount) { + modifiedRequest = setRetryCountHeader(modifiedRequest, retries) + } + + if (!isRetryable(modifiedRequest)) { + return httpClient.execute(modifiedRequest, requestOptions) + } + + val response = + try { + val response = httpClient.execute(modifiedRequest, requestOptions) + if (++retries > maxRetries || !shouldRetry(response)) { + return response + } + + response + } catch (throwable: Throwable) { + if (++retries > maxRetries || !shouldRetry(throwable)) { + throw throwable + } + + null + } + + val backoffDuration = getRetryBackoffDuration(retries, response) + // All responses must be closed, so close the failed one before retrying. + response?.close() + sleeper.sleep(backoffDuration) + } + } + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture { + val modifiedRequest = maybeAddIdempotencyHeader(request) + + // Don't send the current retry count in the headers if the caller set their own value. + val shouldSendRetryCount = + !modifiedRequest.headers.names().contains("X-Stainless-Retry-Count") + + var retries = 0 + + fun executeWithRetries( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture { + val requestWithRetryCount = + if (shouldSendRetryCount) setRetryCountHeader(request, retries) else request + + val responseFuture = httpClient.executeAsync(requestWithRetryCount, requestOptions) + if (!isRetryable(requestWithRetryCount)) { + return responseFuture + } + + return responseFuture + .handleAsync( + fun( + response: HttpResponse?, + throwable: Throwable?, + ): CompletableFuture { + if (response != null) { + if (++retries > maxRetries || !shouldRetry(response)) { + return CompletableFuture.completedFuture(response) + } + } else { + if (++retries > maxRetries || !shouldRetry(throwable!!)) { + val failedFuture = CompletableFuture() + failedFuture.completeExceptionally(throwable) + return failedFuture + } + } + + val backoffDuration = getRetryBackoffDuration(retries, response) + // All responses must be closed, so close the failed one before retrying. + response?.close() + return sleeper.sleepAsync(backoffDuration).thenCompose { + executeWithRetries(requestWithRetryCount, requestOptions) + } + } + ) { + // Run in the same thread. + it.run() + } + .thenCompose(Function.identity()) + } + + return executeWithRetries(modifiedRequest, requestOptions) + } + + override fun close() { + httpClient.close() + sleeper.close() + } + + private fun isRetryable(request: HttpRequest): Boolean = + // Some requests, such as when a request body is being streamed, cannot be retried because + // the body data aren't available on subsequent attempts. + request.body?.repeatable() ?: true + + private fun setRetryCountHeader(request: HttpRequest, retries: Int): HttpRequest = + request.toBuilder().replaceHeaders("X-Stainless-Retry-Count", retries.toString()).build() + + private fun idempotencyKey(): String = "stainless-java-retry-${UUID.randomUUID()}" + + private fun maybeAddIdempotencyHeader(request: HttpRequest): HttpRequest { + if (idempotencyHeader == null || request.headers.names().contains(idempotencyHeader)) { + return request + } + + return request + .toBuilder() + // Set a header to uniquely identify the request when retried. + .putHeader(idempotencyHeader, idempotencyKey()) + .build() + } + + private fun shouldRetry(response: HttpResponse): Boolean { + // Note: this is not a standard header + val shouldRetryHeader = response.headers().values("X-Should-Retry").getOrNull(0) + val statusCode = response.statusCode() + + return when { + // If the server explicitly says whether to retry, obey + shouldRetryHeader == "true" -> true + shouldRetryHeader == "false" -> false + + // Retry on request timeouts + statusCode == 408 -> true + // Retry on lock timeouts + statusCode == 409 -> true + // Retry on rate limits + statusCode == 429 -> true + // Retry internal errors + statusCode >= 500 -> true + else -> false + } + } + + private fun shouldRetry(throwable: Throwable): Boolean = + // Only retry known retryable exceptions, other exceptions are not intended to be retried. + throwable is IOException || + throwable is StagehandIoException || + throwable is StagehandRetryableException + + private fun getRetryBackoffDuration(retries: Int, response: HttpResponse?): Duration { + // About the Retry-After header: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + response + ?.headers() + ?.let { headers -> + headers + .values("Retry-After-Ms") + .getOrNull(0) + ?.toFloatOrNull() + ?.times(TimeUnit.MILLISECONDS.toNanos(1)) + ?: headers.values("Retry-After").getOrNull(0)?.let { retryAfter -> + retryAfter.toFloatOrNull()?.times(TimeUnit.SECONDS.toNanos(1)) + ?: try { + ChronoUnit.NANOS.between( + OffsetDateTime.now(clock), + OffsetDateTime.parse( + retryAfter, + DateTimeFormatter.RFC_1123_DATE_TIME, + ), + ) + } catch (e: DateTimeParseException) { + null + } + } + } + ?.let { retryAfterNanos -> + // If the API asks us to wait a certain amount of time, do what it says. + return Duration.ofNanos(retryAfterNanos.toLong()) + } + + // Apply exponential backoff, but not more than the max. + val backoffSeconds = min(0.5 * 2.0.pow(retries - 1), 8.0) + + // Apply some jitter + val jitter = 1.0 - 0.25 * ThreadLocalRandom.current().nextDouble() + + return Duration.ofNanos((TimeUnit.SECONDS.toNanos(1) * backoffSeconds * jitter).toLong()) + } + + companion object { + + @JvmStatic fun builder() = Builder() + } + + class Builder internal constructor() { + + private var httpClient: HttpClient? = null + private var sleeper: Sleeper? = null + private var clock: Clock = Clock.systemUTC() + private var maxRetries: Int = 2 + private var idempotencyHeader: String? = null + + fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient } + + fun sleeper(sleeper: Sleeper) = apply { this.sleeper = sleeper } + + fun clock(clock: Clock) = apply { this.clock = clock } + + fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries } + + fun idempotencyHeader(header: String) = apply { this.idempotencyHeader = header } + + fun build(): HttpClient = + RetryingHttpClient( + checkRequired("httpClient", httpClient), + sleeper ?: DefaultSleeper(), + clock, + maxRetries, + idempotencyHeader, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/SseMessage.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/SseMessage.kt new file mode 100644 index 0000000..21cc44e --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/SseMessage.kt @@ -0,0 +1,74 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.http + +import com.browserbase.api.errors.StagehandInvalidDataException +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import java.util.Objects + +internal class SseMessage +private constructor( + val jsonMapper: JsonMapper, + val event: String?, + val data: String, + val id: String?, + val retry: Int?, +) { + + companion object { + @JvmStatic fun builder() = Builder() + } + + class Builder internal constructor() { + + private var jsonMapper: JsonMapper? = null + private var event: String? = null + private var data: String = "" + private var id: String? = null + private var retry: Int? = null + + fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper } + + fun event(event: String?) = apply { this.event = event } + + fun data(data: String) = apply { this.data = data } + + fun id(id: String?) = apply { this.id = id } + + fun retry(retry: Int?) = apply { this.retry = retry } + + fun build(): SseMessage = SseMessage(jsonMapper!!, event, data, id, retry) + } + + inline fun json(): T = + try { + jsonMapper.readerFor(jacksonTypeRef()).readValue(jsonNode) + } catch (e: Exception) { + throw StagehandInvalidDataException("Error reading response", e) + } + + private val jsonNode by lazy { + try { + jsonMapper.readTree(data) + } catch (e: Exception) { + throw StagehandInvalidDataException("Error reading response", e) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SseMessage && + event == other.event && + data == other.data && + id == other.id && + retry == other.retry + } + + override fun hashCode(): Int = Objects.hash(event, data, id, retry) + + override fun toString(): String = "SseMessage{event=$event, data=$data, id=$id, retry=$retry}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/StreamResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/StreamResponse.kt new file mode 100644 index 0000000..603e8bb --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/StreamResponse.kt @@ -0,0 +1,19 @@ +package com.browserbase.api.core.http + +import java.util.stream.Stream + +interface StreamResponse : AutoCloseable { + + fun stream(): Stream + + /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */ + override fun close() +} + +@JvmSynthetic +internal fun StreamResponse.map(transform: (T) -> R): StreamResponse = + object : StreamResponse { + override fun stream(): Stream = this@map.stream().map(transform) + + override fun close() = this@map.close() + } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/BadRequestException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/BadRequestException.kt new file mode 100644 index 0000000..3f21e7c --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/BadRequestException.kt @@ -0,0 +1,84 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class BadRequestException +private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) : + StagehandServiceException( + "400: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}", + cause, + ) { + + override fun statusCode(): Int = 400 + + override fun headers(): Headers = headers + + override fun body(): JsonValue = body + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [BadRequestException]. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [BadRequestException]. */ + class Builder internal constructor() { + + private var headers: Headers? = null + private var body: JsonValue? = null + private var cause: Throwable? = null + + @JvmSynthetic + internal fun from(badRequestException: BadRequestException) = apply { + headers = badRequestException.headers + body = badRequestException.body + cause = badRequestException.cause + } + + fun headers(headers: Headers) = apply { this.headers = headers } + + fun body(body: JsonValue) = apply { this.body = body } + + fun cause(cause: Throwable?) = apply { this.cause = cause } + + /** Alias for calling [Builder.cause] with `cause.orElse(null)`. */ + fun cause(cause: Optional) = cause(cause.getOrNull()) + + /** + * Returns an immutable instance of [BadRequestException]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): BadRequestException = + BadRequestException( + checkRequired("headers", headers), + checkRequired("body", body), + cause, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/InternalServerException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/InternalServerException.kt new file mode 100644 index 0000000..1f782c0 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/InternalServerException.kt @@ -0,0 +1,96 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class InternalServerException +private constructor( + private val statusCode: Int, + private val headers: Headers, + private val body: JsonValue, + cause: Throwable?, +) : + StagehandServiceException( + "$statusCode: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}", + cause, + ) { + + override fun statusCode(): Int = statusCode + + override fun headers(): Headers = headers + + override fun body(): JsonValue = body + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [InternalServerException]. + * + * The following fields are required: + * ```java + * .statusCode() + * .headers() + * .body() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [InternalServerException]. */ + class Builder internal constructor() { + + private var statusCode: Int? = null + private var headers: Headers? = null + private var body: JsonValue? = null + private var cause: Throwable? = null + + @JvmSynthetic + internal fun from(internalServerException: InternalServerException) = apply { + statusCode = internalServerException.statusCode + headers = internalServerException.headers + body = internalServerException.body + cause = internalServerException.cause + } + + fun statusCode(statusCode: Int) = apply { this.statusCode = statusCode } + + fun headers(headers: Headers) = apply { this.headers = headers } + + fun body(body: JsonValue) = apply { this.body = body } + + fun cause(cause: Throwable?) = apply { this.cause = cause } + + /** Alias for calling [Builder.cause] with `cause.orElse(null)`. */ + fun cause(cause: Optional) = cause(cause.getOrNull()) + + /** + * Returns an immutable instance of [InternalServerException]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .statusCode() + * .headers() + * .body() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): InternalServerException = + InternalServerException( + checkRequired("statusCode", statusCode), + checkRequired("headers", headers), + checkRequired("body", body), + cause, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/NotFoundException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/NotFoundException.kt new file mode 100644 index 0000000..b3e3f6f --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/NotFoundException.kt @@ -0,0 +1,80 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class NotFoundException +private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) : + StagehandServiceException( + "404: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}", + cause, + ) { + + override fun statusCode(): Int = 404 + + override fun headers(): Headers = headers + + override fun body(): JsonValue = body + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [NotFoundException]. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [NotFoundException]. */ + class Builder internal constructor() { + + private var headers: Headers? = null + private var body: JsonValue? = null + private var cause: Throwable? = null + + @JvmSynthetic + internal fun from(notFoundException: NotFoundException) = apply { + headers = notFoundException.headers + body = notFoundException.body + cause = notFoundException.cause + } + + fun headers(headers: Headers) = apply { this.headers = headers } + + fun body(body: JsonValue) = apply { this.body = body } + + fun cause(cause: Throwable?) = apply { this.cause = cause } + + /** Alias for calling [Builder.cause] with `cause.orElse(null)`. */ + fun cause(cause: Optional) = cause(cause.getOrNull()) + + /** + * Returns an immutable instance of [NotFoundException]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): NotFoundException = + NotFoundException(checkRequired("headers", headers), checkRequired("body", body), cause) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/PermissionDeniedException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/PermissionDeniedException.kt new file mode 100644 index 0000000..79c4ddb --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/PermissionDeniedException.kt @@ -0,0 +1,84 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class PermissionDeniedException +private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) : + StagehandServiceException( + "403: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}", + cause, + ) { + + override fun statusCode(): Int = 403 + + override fun headers(): Headers = headers + + override fun body(): JsonValue = body + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [PermissionDeniedException]. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [PermissionDeniedException]. */ + class Builder internal constructor() { + + private var headers: Headers? = null + private var body: JsonValue? = null + private var cause: Throwable? = null + + @JvmSynthetic + internal fun from(permissionDeniedException: PermissionDeniedException) = apply { + headers = permissionDeniedException.headers + body = permissionDeniedException.body + cause = permissionDeniedException.cause + } + + fun headers(headers: Headers) = apply { this.headers = headers } + + fun body(body: JsonValue) = apply { this.body = body } + + fun cause(cause: Throwable?) = apply { this.cause = cause } + + /** Alias for calling [Builder.cause] with `cause.orElse(null)`. */ + fun cause(cause: Optional) = cause(cause.getOrNull()) + + /** + * Returns an immutable instance of [PermissionDeniedException]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): PermissionDeniedException = + PermissionDeniedException( + checkRequired("headers", headers), + checkRequired("body", body), + cause, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/RateLimitException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/RateLimitException.kt new file mode 100644 index 0000000..34c3d23 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/RateLimitException.kt @@ -0,0 +1,84 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class RateLimitException +private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) : + StagehandServiceException( + "429: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}", + cause, + ) { + + override fun statusCode(): Int = 429 + + override fun headers(): Headers = headers + + override fun body(): JsonValue = body + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [RateLimitException]. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [RateLimitException]. */ + class Builder internal constructor() { + + private var headers: Headers? = null + private var body: JsonValue? = null + private var cause: Throwable? = null + + @JvmSynthetic + internal fun from(rateLimitException: RateLimitException) = apply { + headers = rateLimitException.headers + body = rateLimitException.body + cause = rateLimitException.cause + } + + fun headers(headers: Headers) = apply { this.headers = headers } + + fun body(body: JsonValue) = apply { this.body = body } + + fun cause(cause: Throwable?) = apply { this.cause = cause } + + /** Alias for calling [Builder.cause] with `cause.orElse(null)`. */ + fun cause(cause: Optional) = cause(cause.getOrNull()) + + /** + * Returns an immutable instance of [RateLimitException]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): RateLimitException = + RateLimitException( + checkRequired("headers", headers), + checkRequired("body", body), + cause, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/SseException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/SseException.kt new file mode 100644 index 0000000..8c902f5 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/SseException.kt @@ -0,0 +1,96 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class SseException +private constructor( + private val statusCode: Int, + private val headers: Headers, + private val body: JsonValue, + cause: Throwable?, +) : + StagehandServiceException( + "$statusCode: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}", + cause, + ) { + + override fun statusCode(): Int = statusCode + + override fun headers(): Headers = headers + + override fun body(): JsonValue = body + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [SseException]. + * + * The following fields are required: + * ```java + * .statusCode() + * .headers() + * .body() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SseException]. */ + class Builder internal constructor() { + + private var statusCode: Int? = null + private var headers: Headers? = null + private var body: JsonValue? = null + private var cause: Throwable? = null + + @JvmSynthetic + internal fun from(sseException: SseException) = apply { + statusCode = sseException.statusCode + headers = sseException.headers + body = sseException.body + cause = sseException.cause + } + + fun statusCode(statusCode: Int) = apply { this.statusCode = statusCode } + + fun headers(headers: Headers) = apply { this.headers = headers } + + fun body(body: JsonValue) = apply { this.body = body } + + fun cause(cause: Throwable?) = apply { this.cause = cause } + + /** Alias for calling [Builder.cause] with `cause.orElse(null)`. */ + fun cause(cause: Optional) = cause(cause.getOrNull()) + + /** + * Returns an immutable instance of [SseException]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .statusCode() + * .headers() + * .body() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SseException = + SseException( + checkRequired("statusCode", statusCode), + checkRequired("headers", headers), + checkRequired("body", body), + cause, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandException.kt new file mode 100644 index 0000000..214a6a8 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandException.kt @@ -0,0 +1,5 @@ +package com.browserbase.api.errors + +open class StagehandException +@JvmOverloads +constructor(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandInvalidDataException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandInvalidDataException.kt new file mode 100644 index 0000000..87fe2d4 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandInvalidDataException.kt @@ -0,0 +1,5 @@ +package com.browserbase.api.errors + +class StagehandInvalidDataException +@JvmOverloads +constructor(message: String? = null, cause: Throwable? = null) : StagehandException(message, cause) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandIoException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandIoException.kt new file mode 100644 index 0000000..216ae66 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandIoException.kt @@ -0,0 +1,5 @@ +package com.browserbase.api.errors + +class StagehandIoException +@JvmOverloads +constructor(message: String? = null, cause: Throwable? = null) : StagehandException(message, cause) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandRetryableException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandRetryableException.kt new file mode 100644 index 0000000..18b8aca --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandRetryableException.kt @@ -0,0 +1,14 @@ +package com.browserbase.api.errors + +/** + * Exception that indicates a transient error that can be retried. + * + * When this exception is thrown during an HTTP request, the SDK will automatically retry the + * request up to the maximum number of retries. + * + * @param message A descriptive error message + * @param cause The underlying cause of this exception, if any + */ +class StagehandRetryableException +@JvmOverloads +constructor(message: String? = null, cause: Throwable? = null) : StagehandException(message, cause) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandServiceException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandServiceException.kt new file mode 100644 index 0000000..71ce4fa --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/StagehandServiceException.kt @@ -0,0 +1,17 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.http.Headers + +abstract class StagehandServiceException +protected constructor(message: String, cause: Throwable? = null) : + StagehandException(message, cause) { + + abstract fun statusCode(): Int + + abstract fun headers(): Headers + + abstract fun body(): JsonValue +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnauthorizedException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnauthorizedException.kt new file mode 100644 index 0000000..37597f5 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnauthorizedException.kt @@ -0,0 +1,84 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class UnauthorizedException +private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) : + StagehandServiceException( + "401: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}", + cause, + ) { + + override fun statusCode(): Int = 401 + + override fun headers(): Headers = headers + + override fun body(): JsonValue = body + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [UnauthorizedException]. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [UnauthorizedException]. */ + class Builder internal constructor() { + + private var headers: Headers? = null + private var body: JsonValue? = null + private var cause: Throwable? = null + + @JvmSynthetic + internal fun from(unauthorizedException: UnauthorizedException) = apply { + headers = unauthorizedException.headers + body = unauthorizedException.body + cause = unauthorizedException.cause + } + + fun headers(headers: Headers) = apply { this.headers = headers } + + fun body(body: JsonValue) = apply { this.body = body } + + fun cause(cause: Throwable?) = apply { this.cause = cause } + + /** Alias for calling [Builder.cause] with `cause.orElse(null)`. */ + fun cause(cause: Optional) = cause(cause.getOrNull()) + + /** + * Returns an immutable instance of [UnauthorizedException]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): UnauthorizedException = + UnauthorizedException( + checkRequired("headers", headers), + checkRequired("body", body), + cause, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnexpectedStatusCodeException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnexpectedStatusCodeException.kt new file mode 100644 index 0000000..e013e77 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnexpectedStatusCodeException.kt @@ -0,0 +1,97 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class UnexpectedStatusCodeException +private constructor( + private val statusCode: Int, + private val headers: Headers, + private val body: JsonValue, + cause: Throwable?, +) : + StagehandServiceException( + "$statusCode: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}", + cause, + ) { + + override fun statusCode(): Int = statusCode + + override fun headers(): Headers = headers + + override fun body(): JsonValue = body + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of + * [UnexpectedStatusCodeException]. + * + * The following fields are required: + * ```java + * .statusCode() + * .headers() + * .body() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [UnexpectedStatusCodeException]. */ + class Builder internal constructor() { + + private var statusCode: Int? = null + private var headers: Headers? = null + private var body: JsonValue? = null + private var cause: Throwable? = null + + @JvmSynthetic + internal fun from(unexpectedStatusCodeException: UnexpectedStatusCodeException) = apply { + statusCode = unexpectedStatusCodeException.statusCode + headers = unexpectedStatusCodeException.headers + body = unexpectedStatusCodeException.body + cause = unexpectedStatusCodeException.cause + } + + fun statusCode(statusCode: Int) = apply { this.statusCode = statusCode } + + fun headers(headers: Headers) = apply { this.headers = headers } + + fun body(body: JsonValue) = apply { this.body = body } + + fun cause(cause: Throwable?) = apply { this.cause = cause } + + /** Alias for calling [Builder.cause] with `cause.orElse(null)`. */ + fun cause(cause: Optional) = cause(cause.getOrNull()) + + /** + * Returns an immutable instance of [UnexpectedStatusCodeException]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .statusCode() + * .headers() + * .body() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): UnexpectedStatusCodeException = + UnexpectedStatusCodeException( + checkRequired("statusCode", statusCode), + checkRequired("headers", headers), + checkRequired("body", body), + cause, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnprocessableEntityException.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnprocessableEntityException.kt new file mode 100644 index 0000000..233efac --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/errors/UnprocessableEntityException.kt @@ -0,0 +1,84 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.errors + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class UnprocessableEntityException +private constructor(private val headers: Headers, private val body: JsonValue, cause: Throwable?) : + StagehandServiceException( + "422: ${if (body.isMissing()) "Unknown" else jsonMapper().writeValueAsString(body)}", + cause, + ) { + + override fun statusCode(): Int = 422 + + override fun headers(): Headers = headers + + override fun body(): JsonValue = body + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [UnprocessableEntityException]. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [UnprocessableEntityException]. */ + class Builder internal constructor() { + + private var headers: Headers? = null + private var body: JsonValue? = null + private var cause: Throwable? = null + + @JvmSynthetic + internal fun from(unprocessableEntityException: UnprocessableEntityException) = apply { + headers = unprocessableEntityException.headers + body = unprocessableEntityException.body + cause = unprocessableEntityException.cause + } + + fun headers(headers: Headers) = apply { this.headers = headers } + + fun body(body: JsonValue) = apply { this.body = body } + + fun cause(cause: Throwable?) = apply { this.cause = cause } + + /** Alias for calling [Builder.cause] with `cause.orElse(null)`. */ + fun cause(cause: Optional) = cause(cause.getOrNull()) + + /** + * Returns an immutable instance of [UnprocessableEntityException]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .headers() + * .body() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): UnprocessableEntityException = + UnprocessableEntityException( + checkRequired("headers", headers), + checkRequired("body", body), + cause, + ) + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/Action.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/Action.kt new file mode 100644 index 0000000..a77aa3d --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/Action.kt @@ -0,0 +1,355 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +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.checkKnown +import com.browserbase.api.core.checkRequired +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** Action object returned by observe and used by act */ +class Action +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val description: JsonField, + private val selector: JsonField, + private val arguments: JsonField>, + private val backendNodeId: JsonField, + private val method: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("description") + @ExcludeMissing + description: JsonField = JsonMissing.of(), + @JsonProperty("selector") @ExcludeMissing selector: JsonField = JsonMissing.of(), + @JsonProperty("arguments") + @ExcludeMissing + arguments: JsonField> = JsonMissing.of(), + @JsonProperty("backendNodeId") + @ExcludeMissing + backendNodeId: JsonField = JsonMissing.of(), + @JsonProperty("method") @ExcludeMissing method: JsonField = JsonMissing.of(), + ) : this(description, selector, arguments, backendNodeId, method, mutableMapOf()) + + /** + * Human-readable description of the action + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun description(): String = description.getRequired("description") + + /** + * CSS selector or XPath for the element + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun selector(): String = selector.getRequired("selector") + + /** + * Arguments to pass to the method + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun arguments(): Optional> = arguments.getOptional("arguments") + + /** + * Backend node ID for the element + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun backendNodeId(): Optional = backendNodeId.getOptional("backendNodeId") + + /** + * The method to execute (click, fill, etc.) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun method(): Optional = method.getOptional("method") + + /** + * Returns the raw JSON value of [description]. + * + * Unlike [description], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("description") @ExcludeMissing fun _description(): JsonField = description + + /** + * Returns the raw JSON value of [selector]. + * + * Unlike [selector], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("selector") @ExcludeMissing fun _selector(): JsonField = selector + + /** + * Returns the raw JSON value of [arguments]. + * + * Unlike [arguments], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("arguments") @ExcludeMissing fun _arguments(): JsonField> = arguments + + /** + * Returns the raw JSON value of [backendNodeId]. + * + * Unlike [backendNodeId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("backendNodeId") + @ExcludeMissing + fun _backendNodeId(): JsonField = backendNodeId + + /** + * Returns the raw JSON value of [method]. + * + * Unlike [method], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("method") @ExcludeMissing fun _method(): JsonField = method + + @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 [Action]. + * + * The following fields are required: + * ```java + * .description() + * .selector() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Action]. */ + class Builder internal constructor() { + + private var description: JsonField? = null + private var selector: JsonField? = null + private var arguments: JsonField>? = null + private var backendNodeId: JsonField = JsonMissing.of() + private var method: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(action: Action) = apply { + description = action.description + selector = action.selector + arguments = action.arguments.map { it.toMutableList() } + backendNodeId = action.backendNodeId + method = action.method + additionalProperties = action.additionalProperties.toMutableMap() + } + + /** Human-readable description of the action */ + fun description(description: String) = description(JsonField.of(description)) + + /** + * Sets [Builder.description] to an arbitrary JSON value. + * + * You should usually call [Builder.description] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun description(description: JsonField) = apply { this.description = description } + + /** CSS selector or XPath for the element */ + fun selector(selector: String) = selector(JsonField.of(selector)) + + /** + * Sets [Builder.selector] to an arbitrary JSON value. + * + * You should usually call [Builder.selector] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun selector(selector: JsonField) = apply { this.selector = selector } + + /** Arguments to pass to the method */ + fun arguments(arguments: List) = arguments(JsonField.of(arguments)) + + /** + * Sets [Builder.arguments] to an arbitrary JSON value. + * + * You should usually call [Builder.arguments] with a well-typed `List` value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun arguments(arguments: JsonField>) = apply { + this.arguments = arguments.map { it.toMutableList() } + } + + /** + * Adds a single [String] to [arguments]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addArgument(argument: String) = apply { + arguments = + (arguments ?: JsonField.of(mutableListOf())).also { + checkKnown("arguments", it).add(argument) + } + } + + /** Backend node ID for the element */ + fun backendNodeId(backendNodeId: Double) = backendNodeId(JsonField.of(backendNodeId)) + + /** + * Sets [Builder.backendNodeId] to an arbitrary JSON value. + * + * You should usually call [Builder.backendNodeId] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun backendNodeId(backendNodeId: JsonField) = apply { + this.backendNodeId = backendNodeId + } + + /** The method to execute (click, fill, etc.) */ + fun method(method: String) = method(JsonField.of(method)) + + /** + * Sets [Builder.method] to an arbitrary JSON value. + * + * You should usually call [Builder.method] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun method(method: JsonField) = apply { this.method = method } + + 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 [Action]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .description() + * .selector() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Action = + Action( + checkRequired("description", description), + checkRequired("selector", selector), + (arguments ?: JsonMissing.of()).map { it.toImmutable() }, + backendNodeId, + method, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Action = apply { + if (validated) { + return@apply + } + + description() + selector() + arguments() + backendNodeId() + method() + 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 = + (if (description.asKnown().isPresent) 1 else 0) + + (if (selector.asKnown().isPresent) 1 else 0) + + (arguments.asKnown().getOrNull()?.size ?: 0) + + (if (backendNodeId.asKnown().isPresent) 1 else 0) + + (if (method.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Action && + description == other.description && + selector == other.selector && + arguments == other.arguments && + backendNodeId == other.backendNodeId && + method == other.method && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(description, selector, arguments, backendNodeId, method, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Action{description=$description, selector=$selector, arguments=$arguments, backendNodeId=$backendNodeId, method=$method, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/ModelConfig.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/ModelConfig.kt new file mode 100644 index 0000000..b20ac9d --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/ModelConfig.kt @@ -0,0 +1,591 @@ +// File generated from our OpenAPI spec by Stainless. + +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.checkRequired +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class ModelConfig +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val modelName: JsonField, + private val apiKey: JsonField, + private val baseUrl: JsonField, + private val headers: JsonField, + private val provider: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("modelName") @ExcludeMissing modelName: JsonField = JsonMissing.of(), + @JsonProperty("apiKey") @ExcludeMissing apiKey: JsonField = JsonMissing.of(), + @JsonProperty("baseURL") @ExcludeMissing baseUrl: JsonField = JsonMissing.of(), + @JsonProperty("headers") @ExcludeMissing headers: JsonField = JsonMissing.of(), + @JsonProperty("provider") @ExcludeMissing provider: JsonField = JsonMissing.of(), + ) : this(modelName, apiKey, baseUrl, headers, provider, mutableMapOf()) + + /** + * Model name string with provider prefix (e.g., 'openai/gpt-5-nano') + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun modelName(): String = modelName.getRequired("modelName") + + /** + * API key for the model provider + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun apiKey(): Optional = apiKey.getOptional("apiKey") + + /** + * Base URL for the model provider + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun baseUrl(): Optional = baseUrl.getOptional("baseURL") + + /** + * Custom headers sent with every request to the model provider + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun headers(): Optional = headers.getOptional("headers") + + /** + * AI provider for the model (or provide a baseURL endpoint instead) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun provider(): Optional = provider.getOptional("provider") + + /** + * Returns the raw JSON value of [modelName]. + * + * Unlike [modelName], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("modelName") @ExcludeMissing fun _modelName(): JsonField = modelName + + /** + * Returns the raw JSON value of [apiKey]. + * + * Unlike [apiKey], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("apiKey") @ExcludeMissing fun _apiKey(): JsonField = apiKey + + /** + * Returns the raw JSON value of [baseUrl]. + * + * Unlike [baseUrl], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("baseURL") @ExcludeMissing fun _baseUrl(): JsonField = baseUrl + + /** + * Returns the raw JSON value of [headers]. + * + * Unlike [headers], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("headers") @ExcludeMissing fun _headers(): JsonField = headers + + /** + * Returns the raw JSON value of [provider]. + * + * Unlike [provider], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("provider") @ExcludeMissing fun _provider(): JsonField = provider + + @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 [ModelConfig]. + * + * The following fields are required: + * ```java + * .modelName() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [ModelConfig]. */ + class Builder internal constructor() { + + private var modelName: JsonField? = null + private var apiKey: JsonField = JsonMissing.of() + private var baseUrl: JsonField = JsonMissing.of() + private var headers: JsonField = JsonMissing.of() + private var provider: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(modelConfig: ModelConfig) = apply { + modelName = modelConfig.modelName + apiKey = modelConfig.apiKey + baseUrl = modelConfig.baseUrl + headers = modelConfig.headers + provider = modelConfig.provider + additionalProperties = modelConfig.additionalProperties.toMutableMap() + } + + /** Model name string with provider prefix (e.g., 'openai/gpt-5-nano') */ + fun modelName(modelName: String) = modelName(JsonField.of(modelName)) + + /** + * Sets [Builder.modelName] to an arbitrary JSON value. + * + * You should usually call [Builder.modelName] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun modelName(modelName: JsonField) = apply { this.modelName = modelName } + + /** API key for the model provider */ + fun apiKey(apiKey: String) = apiKey(JsonField.of(apiKey)) + + /** + * Sets [Builder.apiKey] to an arbitrary JSON value. + * + * You should usually call [Builder.apiKey] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun apiKey(apiKey: JsonField) = apply { this.apiKey = apiKey } + + /** Base URL for the model provider */ + fun baseUrl(baseUrl: String) = baseUrl(JsonField.of(baseUrl)) + + /** + * Sets [Builder.baseUrl] to an arbitrary JSON value. + * + * You should usually call [Builder.baseUrl] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun baseUrl(baseUrl: JsonField) = apply { this.baseUrl = baseUrl } + + /** Custom headers sent with every request to the model provider */ + fun headers(headers: Headers) = headers(JsonField.of(headers)) + + /** + * Sets [Builder.headers] to an arbitrary JSON value. + * + * You should usually call [Builder.headers] with a well-typed [Headers] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun headers(headers: JsonField) = apply { this.headers = headers } + + /** AI provider for the model (or provide a baseURL endpoint instead) */ + fun provider(provider: Provider) = provider(JsonField.of(provider)) + + /** + * Sets [Builder.provider] to an arbitrary JSON value. + * + * You should usually call [Builder.provider] with a well-typed [Provider] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun provider(provider: JsonField) = apply { this.provider = provider } + + 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 [ModelConfig]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .modelName() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): ModelConfig = + ModelConfig( + checkRequired("modelName", modelName), + apiKey, + baseUrl, + headers, + provider, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): ModelConfig = apply { + if (validated) { + return@apply + } + + modelName() + apiKey() + baseUrl() + headers().ifPresent { it.validate() } + provider().ifPresent { it.validate() } + 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 = + (if (modelName.asKnown().isPresent) 1 else 0) + + (if (apiKey.asKnown().isPresent) 1 else 0) + + (if (baseUrl.asKnown().isPresent) 1 else 0) + + (headers.asKnown().getOrNull()?.validity() ?: 0) + + (provider.asKnown().getOrNull()?.validity() ?: 0) + + /** Custom headers sent with every request to the model provider */ + class Headers + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Headers]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Headers]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(headers: Headers) = apply { + additionalProperties = headers.additionalProperties.toMutableMap() + } + + 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 [Headers]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Headers = Headers(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Headers = 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 = + additionalProperties.count { (_, value) -> !value.isNull() && !value.isMissing() } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Headers && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Headers{additionalProperties=$additionalProperties}" + } + + /** AI provider for the model (or provide a baseURL endpoint instead) */ + class Provider @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val OPENAI = of("openai") + + @JvmField val ANTHROPIC = of("anthropic") + + @JvmField val GOOGLE = of("google") + + @JvmField val MICROSOFT = of("microsoft") + + @JvmField val BEDROCK = of("bedrock") + + @JvmStatic fun of(value: String) = Provider(JsonField.of(value)) + } + + /** An enum containing [Provider]'s known values. */ + enum class Known { + OPENAI, + ANTHROPIC, + GOOGLE, + MICROSOFT, + BEDROCK, + } + + /** + * An enum containing [Provider]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Provider] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + OPENAI, + ANTHROPIC, + GOOGLE, + MICROSOFT, + BEDROCK, + /** An enum member indicating that [Provider] was instantiated with an unknown value. */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + OPENAI -> Value.OPENAI + ANTHROPIC -> Value.ANTHROPIC + GOOGLE -> Value.GOOGLE + MICROSOFT -> Value.MICROSOFT + BEDROCK -> Value.BEDROCK + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + OPENAI -> Known.OPENAI + ANTHROPIC -> Known.ANTHROPIC + GOOGLE -> Known.GOOGLE + MICROSOFT -> Known.MICROSOFT + BEDROCK -> Known.BEDROCK + else -> throw StagehandInvalidDataException("Unknown Provider: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Provider = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Provider && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is ModelConfig && + modelName == other.modelName && + apiKey == other.apiKey && + baseUrl == other.baseUrl && + headers == other.headers && + provider == other.provider && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(modelName, apiKey, baseUrl, headers, provider, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "ModelConfig{modelName=$modelName, apiKey=$apiKey, baseUrl=$baseUrl, headers=$headers, provider=$provider, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActParams.kt new file mode 100644 index 0000000..a72cbeb --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActParams.kt @@ -0,0 +1,1544 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.BaseDeserializer +import com.browserbase.api.core.BaseSerializer +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.allMaxBy +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.getOrThrow +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 com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import java.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** Executes a browser action using natural language instructions or a predefined Action object. */ +class SessionActParams +private constructor( + private val id: String?, + private val xStreamResponse: XStreamResponse?, + private val body: Body, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, +) : Params { + + /** Unique session identifier */ + fun id(): Optional = Optional.ofNullable(id) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) + + /** + * Natural language instruction or Action object + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun input(): Input = body.input() + + /** + * Target frame ID for the action + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun frameId(): Optional = body.frameId() + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun options(): Optional = body.options() + + /** + * Returns the raw JSON value of [input]. + * + * Unlike [input], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _input(): JsonField = body._input() + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _frameId(): JsonField = body._frameId() + + /** + * Returns the raw JSON value of [options]. + * + * Unlike [options], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _options(): JsonField = body._options() + + fun _additionalBodyProperties(): Map = body._additionalProperties() + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [SessionActParams]. + * + * The following fields are required: + * ```java + * .input() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionActParams]. */ + class Builder internal constructor() { + + private var id: String? = 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() + + @JvmSynthetic + internal fun from(sessionActParams: SessionActParams) = apply { + id = sessionActParams.id + xStreamResponse = sessionActParams.xStreamResponse + body = sessionActParams.body.toBuilder() + additionalHeaders = sessionActParams.additionalHeaders.toBuilder() + additionalQueryParams = sessionActParams.additionalQueryParams.toBuilder() + } + + /** Unique session identifier */ + fun id(id: String?) = apply { this.id = id } + + /** Alias for calling [Builder.id] with `id.orElse(null)`. */ + fun id(id: Optional) = id(id.getOrNull()) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { + this.xStreamResponse = xStreamResponse + } + + /** Alias for calling [Builder.xStreamResponse] with `xStreamResponse.orElse(null)`. */ + 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: + * - [input] + * - [frameId] + * - [options] + */ + fun body(body: Body) = apply { this.body = body.toBuilder() } + + /** Natural language instruction or Action object */ + fun input(input: Input) = apply { body.input(input) } + + /** + * Sets [Builder.input] to an arbitrary JSON value. + * + * You should usually call [Builder.input] with a well-typed [Input] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun input(input: JsonField) = apply { body.input(input) } + + /** Alias for calling [input] with `Input.ofString(string)`. */ + fun input(string: String) = apply { body.input(string) } + + /** Alias for calling [input] with `Input.ofAction(action)`. */ + fun input(action: Action) = apply { body.input(action) } + + /** Target frame ID for the action */ + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun frameId(frameId: JsonField) = apply { body.frameId(frameId) } + + fun options(options: Options) = apply { body.options(options) } + + /** + * Sets [Builder.options] to an arbitrary JSON value. + * + * You should usually call [Builder.options] with a well-typed [Options] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun options(options: JsonField) = apply { body.options(options) } + + 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) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + additionalQueryParams.removeAll(keys) + } + + /** + * Returns an immutable instance of [SessionActParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .input() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionActParams = + SessionActParams( + id, + xStreamResponse, + body.build(), + additionalHeaders.build(), + additionalQueryParams.build(), + ) + } + + fun _body(): Body = body + + fun _pathParam(index: Int): String = + when (index) { + 0 -> id ?: "" + else -> "" + } + + override fun _headers(): Headers = + Headers.builder() + .apply { + xStreamResponse?.let { put("x-stream-response", it.toString()) } + putAll(additionalHeaders) + } + .build() + + override fun _queryParams(): QueryParams = additionalQueryParams + + class Body + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val input: JsonField, + private val frameId: JsonField, + private val options: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("input") @ExcludeMissing input: JsonField = JsonMissing.of(), + @JsonProperty("frameId") @ExcludeMissing frameId: JsonField = JsonMissing.of(), + @JsonProperty("options") @ExcludeMissing options: JsonField = JsonMissing.of(), + ) : this(input, frameId, options, mutableMapOf()) + + /** + * Natural language instruction or Action object + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun input(): Input = input.getRequired("input") + + /** + * Target frame ID for the action + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun frameId(): Optional = frameId.getOptional("frameId") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun options(): Optional = options.getOptional("options") + + /** + * Returns the raw JSON value of [input]. + * + * Unlike [input], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("input") @ExcludeMissing fun _input(): JsonField = input + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("frameId") @ExcludeMissing fun _frameId(): JsonField = frameId + + /** + * Returns the raw JSON value of [options]. + * + * Unlike [options], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("options") @ExcludeMissing fun _options(): JsonField = options + + @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]. + * + * The following fields are required: + * ```java + * .input() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Body]. */ + class Builder internal constructor() { + + private var input: JsonField? = null + private var frameId: JsonField = JsonMissing.of() + private var options: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(body: Body) = apply { + input = body.input + frameId = body.frameId + options = body.options + additionalProperties = body.additionalProperties.toMutableMap() + } + + /** Natural language instruction or Action object */ + fun input(input: Input) = input(JsonField.of(input)) + + /** + * Sets [Builder.input] to an arbitrary JSON value. + * + * You should usually call [Builder.input] with a well-typed [Input] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun input(input: JsonField) = apply { this.input = input } + + /** Alias for calling [input] with `Input.ofString(string)`. */ + fun input(string: String) = input(Input.ofString(string)) + + /** Alias for calling [input] with `Input.ofAction(action)`. */ + fun input(action: Action) = input(Input.ofAction(action)) + + /** Target frame ID for the action */ + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun frameId(frameId: JsonField) = apply { this.frameId = frameId } + + fun options(options: Options) = options(JsonField.of(options)) + + /** + * Sets [Builder.options] to an arbitrary JSON value. + * + * You should usually call [Builder.options] with a well-typed [Options] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun options(options: JsonField) = apply { this.options = options } + + 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. + * + * The following fields are required: + * ```java + * .input() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Body = + Body( + checkRequired("input", input), + frameId, + options, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Body = apply { + if (validated) { + return@apply + } + + input().validate() + frameId() + options().ifPresent { it.validate() } + 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 = + (input.asKnown().getOrNull()?.validity() ?: 0) + + (if (frameId.asKnown().isPresent) 1 else 0) + + (options.asKnown().getOrNull()?.validity() ?: 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Body && + input == other.input && + frameId == other.frameId && + options == other.options && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(input, frameId, options, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Body{input=$input, frameId=$frameId, options=$options, additionalProperties=$additionalProperties}" + } + + /** Natural language instruction or Action object */ + @JsonDeserialize(using = Input.Deserializer::class) + @JsonSerialize(using = Input.Serializer::class) + class Input + private constructor( + private val string: String? = null, + private val action: Action? = null, + private val _json: JsonValue? = null, + ) { + + fun string(): Optional = Optional.ofNullable(string) + + /** Action object returned by observe and used by act */ + fun action(): Optional = Optional.ofNullable(action) + + fun isString(): Boolean = string != null + + fun isAction(): Boolean = action != null + + fun asString(): String = string.getOrThrow("string") + + /** Action object returned by observe and used by act */ + fun asAction(): Action = action.getOrThrow("action") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, unless + * [visitor] overrides [Visitor.unknown]. To handle variants not known to this version of + * the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = input.accept(new Input.Visitor>() { + * @Override + * public Optional visitString(String string) { + * return Optional.of(string.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in [visitor] + * and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + string != null -> visitor.visitString(string) + action != null -> visitor.visitAction(action) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Input = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitString(string: String) {} + + override fun visitAction(action: Action) { + action.validate() + } + } + ) + 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 = + accept( + object : Visitor { + override fun visitString(string: String) = 1 + + override fun visitAction(action: Action) = action.validity() + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Input && string == other.string && action == other.action + } + + override fun hashCode(): Int = Objects.hash(string, action) + + override fun toString(): String = + when { + string != null -> "Input{string=$string}" + action != null -> "Input{action=$action}" + _json != null -> "Input{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Input") + } + + companion object { + + @JvmStatic fun ofString(string: String) = Input(string = string) + + /** Action object returned by observe and used by act */ + @JvmStatic fun ofAction(action: Action) = Input(action = action) + } + + /** An interface that defines how to map each variant of [Input] to a value of type [T]. */ + interface Visitor { + + fun visitString(string: String): T + + /** Action object returned by observe and used by act */ + fun visitAction(action: Action): T + + /** + * Maps an unknown variant of [Input] to a value of type [T]. + * + * An instance of [Input] can contain an unknown variant if it was deserialized from + * data that doesn't match any known variant. For example, if the SDK is on an older + * version than the API, then the API may respond with new variants that the SDK is + * unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Input: $json") + } + } + + internal class Deserializer : BaseDeserializer(Input::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Input { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Input(action = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + Input(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible with + // all the possible variants (e.g. deserializing from boolean). + 0 -> Input(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the first + // completely valid match, or simply the first match if none are completely + // valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Input::class) { + + override fun serialize( + value: Input, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.string != null -> generator.writeObject(value.string) + value.action != null -> generator.writeObject(value.action) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Input") + } + } + } + } + + class Options + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val model: JsonField, + private val timeout: JsonField, + private val variables: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), + @JsonProperty("timeout") @ExcludeMissing timeout: JsonField = JsonMissing.of(), + @JsonProperty("variables") + @ExcludeMissing + variables: JsonField = JsonMissing.of(), + ) : this(model, timeout, variables, mutableMapOf()) + + /** + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun model(): Optional = model.getOptional("model") + + /** + * Timeout in ms for the action + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun timeout(): Optional = timeout.getOptional("timeout") + + /** + * Variables to substitute in the action instruction. Accepts flat primitives or { value, + * description? } objects. + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun variables(): Optional = variables.getOptional("variables") + + /** + * Returns the raw JSON value of [model]. + * + * Unlike [model], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model + + /** + * Returns the raw JSON value of [timeout]. + * + * Unlike [timeout], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("timeout") @ExcludeMissing fun _timeout(): JsonField = timeout + + /** + * Returns the raw JSON value of [variables]. + * + * Unlike [variables], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("variables") + @ExcludeMissing + fun _variables(): JsonField = variables + + @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 [Options]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Options]. */ + class Builder internal constructor() { + + private var model: JsonField = JsonMissing.of() + private var timeout: JsonField = JsonMissing.of() + private var variables: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(options: Options) = apply { + model = options.model + timeout = options.timeout + variables = options.variables + additionalProperties = options.additionalProperties.toMutableMap() + } + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + fun model(model: Model) = model(JsonField.of(model)) + + /** + * Sets [Builder.model] to an arbitrary JSON value. + * + * You should usually call [Builder.model] with a well-typed [Model] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun model(model: JsonField) = apply { this.model = model } + + /** Alias for calling [model] with `Model.ofConfig(config)`. */ + fun model(config: ModelConfig) = model(Model.ofConfig(config)) + + /** Alias for calling [model] with `Model.ofString(string)`. */ + fun model(string: String) = model(Model.ofString(string)) + + /** Timeout in ms for the action */ + fun timeout(timeout: Double) = timeout(JsonField.of(timeout)) + + /** + * Sets [Builder.timeout] to an arbitrary JSON value. + * + * You should usually call [Builder.timeout] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun timeout(timeout: JsonField) = apply { this.timeout = timeout } + + /** + * Variables to substitute in the action instruction. Accepts flat primitives or { + * value, description? } objects. + */ + fun variables(variables: Variables) = variables(JsonField.of(variables)) + + /** + * Sets [Builder.variables] to an arbitrary JSON value. + * + * You should usually call [Builder.variables] with a well-typed [Variables] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun variables(variables: JsonField) = apply { this.variables = variables } + + 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 [Options]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Options = + Options(model, timeout, variables, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Options = apply { + if (validated) { + return@apply + } + + model().ifPresent { it.validate() } + timeout() + variables().ifPresent { it.validate() } + 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 = + (model.asKnown().getOrNull()?.validity() ?: 0) + + (if (timeout.asKnown().isPresent) 1 else 0) + + (variables.asKnown().getOrNull()?.validity() ?: 0) + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + @JsonDeserialize(using = Model.Deserializer::class) + @JsonSerialize(using = Model.Serializer::class) + class Model + private constructor( + private val config: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun config(): Optional = Optional.ofNullable(config) + + fun string(): Optional = Optional.ofNullable(string) + + fun isConfig(): Boolean = config != null + + fun isString(): Boolean = string != null + + fun asConfig(): ModelConfig = config.getOrThrow("config") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given + * [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, + * unless [visitor] overrides [Visitor.unknown]. To handle variants not known to this + * version of the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = model.accept(new Model.Visitor>() { + * @Override + * public Optional visitConfig(ModelConfig config) { + * return Optional.of(config.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in + * [visitor] and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + config != null -> visitor.visitConfig(config) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Model = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) { + config.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) = config.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Model && config == other.config && string == other.string + } + + override fun hashCode(): Int = Objects.hash(config, string) + + override fun toString(): String = + when { + config != null -> "Model{config=$config}" + string != null -> "Model{string=$string}" + _json != null -> "Model{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Model") + } + + companion object { + + @JvmStatic fun ofConfig(config: ModelConfig) = Model(config = config) + + @JvmStatic fun ofString(string: String) = Model(string = string) + } + + /** + * An interface that defines how to map each variant of [Model] to a value of type [T]. + */ + interface Visitor { + + fun visitConfig(config: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [Model] to a value of type [T]. + * + * An instance of [Model] can contain an unknown variant if it was deserialized from + * data that doesn't match any known variant. For example, if the SDK is on an older + * version than the API, then the API may respond with new variants that the SDK is + * unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Model: $json") + } + } + + internal class Deserializer : BaseDeserializer(Model::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Model { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Model(config = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + Model(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> Model(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Model::class) { + + override fun serialize( + value: Model, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.config != null -> generator.writeObject(value.config) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Model") + } + } + } + } + + /** + * Variables to substitute in the action instruction. Accepts flat primitives or { value, + * description? } objects. + */ + class Variables + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Variables]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Variables]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(variables: Variables) = apply { + additionalProperties = variables.additionalProperties.toMutableMap() + } + + 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 [Variables]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Variables = Variables(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Variables = 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 = + additionalProperties.count { (_, value) -> !value.isNull() && !value.isMissing() } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Variables && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Variables{additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Options && + model == other.model && + timeout == other.timeout && + variables == other.variables && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(model, timeout, variables, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Options{model=$model, timeout=$timeout, variables=$variables, additionalProperties=$additionalProperties}" + } + + /** Whether to stream the response via SSE */ + class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val TRUE = of("true") + + @JvmField val FALSE = of("false") + + @JvmStatic fun of(value: String) = XStreamResponse(JsonField.of(value)) + } + + /** An enum containing [XStreamResponse]'s known values. */ + enum class Known { + TRUE, + FALSE, + } + + /** + * An enum containing [XStreamResponse]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [XStreamResponse] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + TRUE, + FALSE, + /** + * An enum member indicating that [XStreamResponse] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + TRUE -> Value.TRUE + FALSE -> Value.FALSE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + TRUE -> Known.TRUE + FALSE -> Known.FALSE + else -> throw StagehandInvalidDataException("Unknown XStreamResponse: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): XStreamResponse = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is XStreamResponse && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionActParams && + id == other.id && + xStreamResponse == other.xStreamResponse && + body == other.body && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams + } + + override fun hashCode(): Int = + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) + + override fun toString() = + "SessionActParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActResponse.kt new file mode 100644 index 0000000..90bf268 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActResponse.kt @@ -0,0 +1,1108 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +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.checkKnown +import com.browserbase.api.core.checkRequired +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class SessionActResponse +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val data: JsonField, + private val success: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("success") @ExcludeMissing success: JsonField = JsonMissing.of(), + ) : this(data, success, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun data(): Data = data.getRequired("data") + + /** + * Indicates whether the request was successful + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [data]. + * + * Unlike [data], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("data") @ExcludeMissing fun _data(): JsonField = data + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @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 [SessionActResponse]. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionActResponse]. */ + class Builder internal constructor() { + + private var data: JsonField? = null + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(sessionActResponse: SessionActResponse) = apply { + data = sessionActResponse.data + success = sessionActResponse.success + additionalProperties = sessionActResponse.additionalProperties.toMutableMap() + } + + fun data(data: Data) = data(JsonField.of(data)) + + /** + * Sets [Builder.data] to an arbitrary JSON value. + * + * You should usually call [Builder.data] with a well-typed [Data] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun data(data: JsonField) = apply { this.data = data } + + /** Indicates whether the request was successful */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [SessionActResponse]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionActResponse = + SessionActResponse( + checkRequired("data", data), + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): SessionActResponse = apply { + if (validated) { + return@apply + } + + data().validate() + success() + 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 = + (data.asKnown().getOrNull()?.validity() ?: 0) + (if (success.asKnown().isPresent) 1 else 0) + + class Data + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val result: JsonField, + private val actionId: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("result") @ExcludeMissing result: JsonField = JsonMissing.of(), + @JsonProperty("actionId") @ExcludeMissing actionId: JsonField = JsonMissing.of(), + ) : this(result, actionId, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun result(): Result = result.getRequired("result") + + /** + * Action ID for tracking + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun actionId(): Optional = actionId.getOptional("actionId") + + /** + * Returns the raw JSON value of [result]. + * + * Unlike [result], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("result") @ExcludeMissing fun _result(): JsonField = result + + /** + * Returns the raw JSON value of [actionId]. + * + * Unlike [actionId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("actionId") @ExcludeMissing fun _actionId(): JsonField = actionId + + @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 [Data]. + * + * The following fields are required: + * ```java + * .result() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Data]. */ + class Builder internal constructor() { + + private var result: JsonField? = null + private var actionId: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(data: Data) = apply { + result = data.result + actionId = data.actionId + additionalProperties = data.additionalProperties.toMutableMap() + } + + fun result(result: Result) = result(JsonField.of(result)) + + /** + * Sets [Builder.result] to an arbitrary JSON value. + * + * You should usually call [Builder.result] with a well-typed [Result] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun result(result: JsonField) = apply { this.result = result } + + /** Action ID for tracking */ + fun actionId(actionId: String) = actionId(JsonField.of(actionId)) + + /** + * Sets [Builder.actionId] to an arbitrary JSON value. + * + * You should usually call [Builder.actionId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun actionId(actionId: JsonField) = apply { this.actionId = actionId } + + 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 [Data]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .result() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Data = + Data(checkRequired("result", result), actionId, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Data = apply { + if (validated) { + return@apply + } + + result().validate() + actionId() + 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 = + (result.asKnown().getOrNull()?.validity() ?: 0) + + (if (actionId.asKnown().isPresent) 1 else 0) + + class Result + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val actionDescription: JsonField, + private val actions: JsonField>, + private val message: JsonField, + private val success: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("actionDescription") + @ExcludeMissing + actionDescription: JsonField = JsonMissing.of(), + @JsonProperty("actions") + @ExcludeMissing + actions: JsonField> = JsonMissing.of(), + @JsonProperty("message") + @ExcludeMissing + message: JsonField = JsonMissing.of(), + @JsonProperty("success") + @ExcludeMissing + success: JsonField = JsonMissing.of(), + ) : this(actionDescription, actions, message, success, mutableMapOf()) + + /** + * Description of the action that was performed + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun actionDescription(): String = actionDescription.getRequired("actionDescription") + + /** + * List of actions that were executed + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun actions(): List = actions.getRequired("actions") + + /** + * Human-readable result message + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun message(): String = message.getRequired("message") + + /** + * Whether the action completed successfully + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [actionDescription]. + * + * Unlike [actionDescription], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("actionDescription") + @ExcludeMissing + fun _actionDescription(): JsonField = actionDescription + + /** + * Returns the raw JSON value of [actions]. + * + * Unlike [actions], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("actions") + @ExcludeMissing + fun _actions(): JsonField> = actions + + /** + * Returns the raw JSON value of [message]. + * + * Unlike [message], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("message") @ExcludeMissing fun _message(): JsonField = message + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @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 [Result]. + * + * The following fields are required: + * ```java + * .actionDescription() + * .actions() + * .message() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Result]. */ + class Builder internal constructor() { + + private var actionDescription: JsonField? = null + private var actions: JsonField>? = null + private var message: JsonField? = null + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(result: Result) = apply { + actionDescription = result.actionDescription + actions = result.actions.map { it.toMutableList() } + message = result.message + success = result.success + additionalProperties = result.additionalProperties.toMutableMap() + } + + /** Description of the action that was performed */ + fun actionDescription(actionDescription: String) = + actionDescription(JsonField.of(actionDescription)) + + /** + * Sets [Builder.actionDescription] to an arbitrary JSON value. + * + * You should usually call [Builder.actionDescription] with a well-typed [String] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun actionDescription(actionDescription: JsonField) = apply { + this.actionDescription = actionDescription + } + + /** List of actions that were executed */ + fun actions(actions: List) = actions(JsonField.of(actions)) + + /** + * Sets [Builder.actions] to an arbitrary JSON value. + * + * You should usually call [Builder.actions] with a well-typed `List` value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun actions(actions: JsonField>) = apply { + this.actions = actions.map { it.toMutableList() } + } + + /** + * Adds a single [Action] to [actions]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addAction(action: Action) = apply { + actions = + (actions ?: JsonField.of(mutableListOf())).also { + checkKnown("actions", it).add(action) + } + } + + /** Human-readable result message */ + fun message(message: String) = message(JsonField.of(message)) + + /** + * Sets [Builder.message] to an arbitrary JSON value. + * + * You should usually call [Builder.message] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun message(message: JsonField) = apply { this.message = message } + + /** Whether the action completed successfully */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [Result]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .actionDescription() + * .actions() + * .message() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Result = + Result( + checkRequired("actionDescription", actionDescription), + checkRequired("actions", actions).map { it.toImmutable() }, + checkRequired("message", message), + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Result = apply { + if (validated) { + return@apply + } + + actionDescription() + actions().forEach { it.validate() } + message() + success() + 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 = + (if (actionDescription.asKnown().isPresent) 1 else 0) + + (actions.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + (if (message.asKnown().isPresent) 1 else 0) + + (if (success.asKnown().isPresent) 1 else 0) + + /** Action object returned by observe and used by act */ + class Action + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val description: JsonField, + private val selector: JsonField, + private val arguments: JsonField>, + private val backendNodeId: JsonField, + private val method: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("description") + @ExcludeMissing + description: JsonField = JsonMissing.of(), + @JsonProperty("selector") + @ExcludeMissing + selector: JsonField = JsonMissing.of(), + @JsonProperty("arguments") + @ExcludeMissing + arguments: JsonField> = JsonMissing.of(), + @JsonProperty("backendNodeId") + @ExcludeMissing + backendNodeId: JsonField = JsonMissing.of(), + @JsonProperty("method") + @ExcludeMissing + method: JsonField = JsonMissing.of(), + ) : this(description, selector, arguments, backendNodeId, method, mutableMapOf()) + + /** + * Human-readable description of the action + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun description(): String = description.getRequired("description") + + /** + * CSS selector or XPath for the element + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun selector(): String = selector.getRequired("selector") + + /** + * Arguments to pass to the method + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun arguments(): Optional> = arguments.getOptional("arguments") + + /** + * Backend node ID for the element + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun backendNodeId(): Optional = backendNodeId.getOptional("backendNodeId") + + /** + * The method to execute (click, fill, etc.) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun method(): Optional = method.getOptional("method") + + /** + * Returns the raw JSON value of [description]. + * + * Unlike [description], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("description") + @ExcludeMissing + fun _description(): JsonField = description + + /** + * Returns the raw JSON value of [selector]. + * + * Unlike [selector], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("selector") + @ExcludeMissing + fun _selector(): JsonField = selector + + /** + * Returns the raw JSON value of [arguments]. + * + * Unlike [arguments], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("arguments") + @ExcludeMissing + fun _arguments(): JsonField> = arguments + + /** + * Returns the raw JSON value of [backendNodeId]. + * + * Unlike [backendNodeId], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("backendNodeId") + @ExcludeMissing + fun _backendNodeId(): JsonField = backendNodeId + + /** + * Returns the raw JSON value of [method]. + * + * Unlike [method], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("method") @ExcludeMissing fun _method(): JsonField = method + + @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 [Action]. + * + * The following fields are required: + * ```java + * .description() + * .selector() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Action]. */ + class Builder internal constructor() { + + private var description: JsonField? = null + private var selector: JsonField? = null + private var arguments: JsonField>? = null + private var backendNodeId: JsonField = JsonMissing.of() + private var method: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(action: Action) = apply { + description = action.description + selector = action.selector + arguments = action.arguments.map { it.toMutableList() } + backendNodeId = action.backendNodeId + method = action.method + additionalProperties = action.additionalProperties.toMutableMap() + } + + /** Human-readable description of the action */ + fun description(description: String) = description(JsonField.of(description)) + + /** + * Sets [Builder.description] to an arbitrary JSON value. + * + * You should usually call [Builder.description] with a well-typed [String] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun description(description: JsonField) = apply { + this.description = description + } + + /** CSS selector or XPath for the element */ + fun selector(selector: String) = selector(JsonField.of(selector)) + + /** + * Sets [Builder.selector] to an arbitrary JSON value. + * + * You should usually call [Builder.selector] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun selector(selector: JsonField) = apply { this.selector = selector } + + /** Arguments to pass to the method */ + fun arguments(arguments: List) = arguments(JsonField.of(arguments)) + + /** + * Sets [Builder.arguments] to an arbitrary JSON value. + * + * You should usually call [Builder.arguments] with a well-typed `List` + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun arguments(arguments: JsonField>) = apply { + this.arguments = arguments.map { it.toMutableList() } + } + + /** + * Adds a single [String] to [arguments]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addArgument(argument: String) = apply { + arguments = + (arguments ?: JsonField.of(mutableListOf())).also { + checkKnown("arguments", it).add(argument) + } + } + + /** Backend node ID for the element */ + fun backendNodeId(backendNodeId: Double) = + backendNodeId(JsonField.of(backendNodeId)) + + /** + * Sets [Builder.backendNodeId] to an arbitrary JSON value. + * + * You should usually call [Builder.backendNodeId] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun backendNodeId(backendNodeId: JsonField) = apply { + this.backendNodeId = backendNodeId + } + + /** The method to execute (click, fill, etc.) */ + fun method(method: String) = method(JsonField.of(method)) + + /** + * Sets [Builder.method] to an arbitrary JSON value. + * + * You should usually call [Builder.method] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun method(method: JsonField) = apply { this.method = method } + + 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 [Action]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .description() + * .selector() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Action = + Action( + checkRequired("description", description), + checkRequired("selector", selector), + (arguments ?: JsonMissing.of()).map { it.toImmutable() }, + backendNodeId, + method, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Action = apply { + if (validated) { + return@apply + } + + description() + selector() + arguments() + backendNodeId() + method() + 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 = + (if (description.asKnown().isPresent) 1 else 0) + + (if (selector.asKnown().isPresent) 1 else 0) + + (arguments.asKnown().getOrNull()?.size ?: 0) + + (if (backendNodeId.asKnown().isPresent) 1 else 0) + + (if (method.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Action && + description == other.description && + selector == other.selector && + arguments == other.arguments && + backendNodeId == other.backendNodeId && + method == other.method && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + description, + selector, + arguments, + backendNodeId, + method, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Action{description=$description, selector=$selector, arguments=$arguments, backendNodeId=$backendNodeId, method=$method, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Result && + actionDescription == other.actionDescription && + actions == other.actions && + message == other.message && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(actionDescription, actions, message, success, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Result{actionDescription=$actionDescription, actions=$actions, message=$message, success=$success, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Data && + result == other.result && + actionId == other.actionId && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(result, actionId, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Data{result=$result, actionId=$actionId, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionActResponse && + data == other.data && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(data, success, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "SessionActResponse{data=$data, success=$success, additionalProperties=$additionalProperties}" +} 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 new file mode 100644 index 0000000..ff13fdc --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndParams.kt @@ -0,0 +1,405 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.Enum +import com.browserbase.api.core.JsonField +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.JsonCreator +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** Terminates the browser session and releases all associated resources. */ +class SessionEndParams +private constructor( + private val id: String?, + private val xStreamResponse: XStreamResponse?, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, + private val additionalBodyProperties: Map, +) : Params { + + /** Unique session identifier */ + fun id(): Optional = Optional.ofNullable(id) + + /** 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 + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + @JvmStatic fun none(): SessionEndParams = builder().build() + + /** Returns a mutable builder for constructing an instance of [SessionEndParams]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionEndParams]. */ + class Builder internal constructor() { + + private var id: String? = null + private var xStreamResponse: XStreamResponse? = null + 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 { + id = sessionEndParams.id + xStreamResponse = sessionEndParams.xStreamResponse + additionalHeaders = sessionEndParams.additionalHeaders.toBuilder() + additionalQueryParams = sessionEndParams.additionalQueryParams.toBuilder() + additionalBodyProperties = sessionEndParams.additionalBodyProperties.toMutableMap() + } + + /** Unique session identifier */ + fun id(id: String?) = apply { this.id = id } + + /** Alias for calling [Builder.id] with `id.orElse(null)`. */ + fun id(id: Optional) = id(id.getOrNull()) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { + this.xStreamResponse = xStreamResponse + } + + /** Alias for calling [Builder.xStreamResponse] with `xStreamResponse.orElse(null)`. */ + fun xStreamResponse(xStreamResponse: Optional) = + xStreamResponse(xStreamResponse.getOrNull()) + + fun additionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + 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]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): SessionEndParams = + SessionEndParams( + id, + xStreamResponse, + additionalHeaders.build(), + additionalQueryParams.build(), + additionalBodyProperties.toImmutable(), + ) + } + + fun _body(): Optional> = + Optional.ofNullable(additionalBodyProperties.ifEmpty { null }) + + fun _pathParam(index: Int): String = + when (index) { + 0 -> id ?: "" + else -> "" + } + + override fun _headers(): Headers = + Headers.builder() + .apply { + xStreamResponse?.let { put("x-stream-response", it.toString()) } + putAll(additionalHeaders) + } + .build() + + override fun _queryParams(): QueryParams = additionalQueryParams + + /** Whether to stream the response via SSE */ + class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val TRUE = of("true") + + @JvmField val FALSE = of("false") + + @JvmStatic fun of(value: String) = XStreamResponse(JsonField.of(value)) + } + + /** An enum containing [XStreamResponse]'s known values. */ + enum class Known { + TRUE, + FALSE, + } + + /** + * An enum containing [XStreamResponse]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [XStreamResponse] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + TRUE, + FALSE, + /** + * An enum member indicating that [XStreamResponse] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + TRUE -> Value.TRUE + FALSE -> Value.FALSE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + TRUE -> Known.TRUE + FALSE -> Known.FALSE + else -> throw StagehandInvalidDataException("Unknown XStreamResponse: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): XStreamResponse = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is XStreamResponse && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionEndParams && + id == other.id && + xStreamResponse == other.xStreamResponse && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams && + additionalBodyProperties == other.additionalBodyProperties + } + + override fun hashCode(): Int = + Objects.hash( + id, + xStreamResponse, + additionalHeaders, + additionalQueryParams, + additionalBodyProperties, + ) + + override fun toString() = + "SessionEndParams{id=$id, xStreamResponse=$xStreamResponse, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams, additionalBodyProperties=$additionalBodyProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndResponse.kt new file mode 100644 index 0000000..d3f370f --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndResponse.kt @@ -0,0 +1,181 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +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.checkRequired +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.util.Collections +import java.util.Objects + +class SessionEndResponse +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val success: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("success") @ExcludeMissing success: JsonField = JsonMissing.of() + ) : this(success, mutableMapOf()) + + /** + * Indicates whether the request was successful + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @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 [SessionEndResponse]. + * + * The following fields are required: + * ```java + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionEndResponse]. */ + class Builder internal constructor() { + + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(sessionEndResponse: SessionEndResponse) = apply { + success = sessionEndResponse.success + additionalProperties = sessionEndResponse.additionalProperties.toMutableMap() + } + + /** Indicates whether the request was successful */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [SessionEndResponse]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionEndResponse = + SessionEndResponse( + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): SessionEndResponse = apply { + if (validated) { + return@apply + } + + success() + 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 = (if (success.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionEndResponse && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(success, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "SessionEndResponse{success=$success, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteParams.kt new file mode 100644 index 0000000..d124ba0 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteParams.kt @@ -0,0 +1,2465 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.BaseDeserializer +import com.browserbase.api.core.BaseSerializer +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.allMaxBy +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.getOrThrow +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 com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import java.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** Runs an autonomous AI agent that can perform complex multi-step browser tasks. */ +class SessionExecuteParams +private constructor( + private val id: String?, + private val xStreamResponse: XStreamResponse?, + private val body: Body, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, +) : Params { + + /** Unique session identifier */ + fun id(): Optional = Optional.ofNullable(id) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun agentConfig(): AgentConfig = body.agentConfig() + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun executeOptions(): ExecuteOptions = body.executeOptions() + + /** + * Target frame ID for the agent + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun frameId(): Optional = body.frameId() + + /** + * If true, the server captures a cache entry and returns it to the client + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun shouldCache(): Optional = body.shouldCache() + + /** + * Returns the raw JSON value of [agentConfig]. + * + * Unlike [agentConfig], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _agentConfig(): JsonField = body._agentConfig() + + /** + * Returns the raw JSON value of [executeOptions]. + * + * Unlike [executeOptions], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _executeOptions(): JsonField = body._executeOptions() + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _frameId(): JsonField = body._frameId() + + /** + * Returns the raw JSON value of [shouldCache]. + * + * Unlike [shouldCache], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _shouldCache(): JsonField = body._shouldCache() + + fun _additionalBodyProperties(): Map = body._additionalProperties() + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [SessionExecuteParams]. + * + * The following fields are required: + * ```java + * .agentConfig() + * .executeOptions() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionExecuteParams]. */ + class Builder internal constructor() { + + private var id: String? = 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() + + @JvmSynthetic + internal fun from(sessionExecuteParams: SessionExecuteParams) = apply { + id = sessionExecuteParams.id + xStreamResponse = sessionExecuteParams.xStreamResponse + body = sessionExecuteParams.body.toBuilder() + additionalHeaders = sessionExecuteParams.additionalHeaders.toBuilder() + additionalQueryParams = sessionExecuteParams.additionalQueryParams.toBuilder() + } + + /** Unique session identifier */ + fun id(id: String?) = apply { this.id = id } + + /** Alias for calling [Builder.id] with `id.orElse(null)`. */ + fun id(id: Optional) = id(id.getOrNull()) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { + this.xStreamResponse = xStreamResponse + } + + /** Alias for calling [Builder.xStreamResponse] with `xStreamResponse.orElse(null)`. */ + 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: + * - [agentConfig] + * - [executeOptions] + * - [frameId] + * - [shouldCache] + */ + fun body(body: Body) = apply { this.body = body.toBuilder() } + + fun agentConfig(agentConfig: AgentConfig) = apply { body.agentConfig(agentConfig) } + + /** + * Sets [Builder.agentConfig] to an arbitrary JSON value. + * + * You should usually call [Builder.agentConfig] with a well-typed [AgentConfig] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun agentConfig(agentConfig: JsonField) = apply { + body.agentConfig(agentConfig) + } + + fun executeOptions(executeOptions: ExecuteOptions) = apply { + body.executeOptions(executeOptions) + } + + /** + * Sets [Builder.executeOptions] to an arbitrary JSON value. + * + * You should usually call [Builder.executeOptions] with a well-typed [ExecuteOptions] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun executeOptions(executeOptions: JsonField) = apply { + body.executeOptions(executeOptions) + } + + /** Target frame ID for the agent */ + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun frameId(frameId: JsonField) = apply { body.frameId(frameId) } + + /** If true, the server captures a cache entry and returns it to the client */ + fun shouldCache(shouldCache: Boolean) = apply { body.shouldCache(shouldCache) } + + /** + * Sets [Builder.shouldCache] to an arbitrary JSON value. + * + * You should usually call [Builder.shouldCache] with a well-typed [Boolean] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun shouldCache(shouldCache: JsonField) = apply { body.shouldCache(shouldCache) } + + 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) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + additionalQueryParams.removeAll(keys) + } + + /** + * Returns an immutable instance of [SessionExecuteParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .agentConfig() + * .executeOptions() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionExecuteParams = + SessionExecuteParams( + id, + xStreamResponse, + body.build(), + additionalHeaders.build(), + additionalQueryParams.build(), + ) + } + + fun _body(): Body = body + + fun _pathParam(index: Int): String = + when (index) { + 0 -> id ?: "" + else -> "" + } + + override fun _headers(): Headers = + Headers.builder() + .apply { + xStreamResponse?.let { put("x-stream-response", it.toString()) } + putAll(additionalHeaders) + } + .build() + + override fun _queryParams(): QueryParams = additionalQueryParams + + class Body + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val agentConfig: JsonField, + private val executeOptions: JsonField, + private val frameId: JsonField, + private val shouldCache: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("agentConfig") + @ExcludeMissing + agentConfig: JsonField = JsonMissing.of(), + @JsonProperty("executeOptions") + @ExcludeMissing + executeOptions: JsonField = JsonMissing.of(), + @JsonProperty("frameId") @ExcludeMissing frameId: JsonField = JsonMissing.of(), + @JsonProperty("shouldCache") + @ExcludeMissing + shouldCache: JsonField = JsonMissing.of(), + ) : this(agentConfig, executeOptions, frameId, shouldCache, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun agentConfig(): AgentConfig = agentConfig.getRequired("agentConfig") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun executeOptions(): ExecuteOptions = executeOptions.getRequired("executeOptions") + + /** + * Target frame ID for the agent + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun frameId(): Optional = frameId.getOptional("frameId") + + /** + * If true, the server captures a cache entry and returns it to the client + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun shouldCache(): Optional = shouldCache.getOptional("shouldCache") + + /** + * Returns the raw JSON value of [agentConfig]. + * + * Unlike [agentConfig], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("agentConfig") + @ExcludeMissing + fun _agentConfig(): JsonField = agentConfig + + /** + * Returns the raw JSON value of [executeOptions]. + * + * Unlike [executeOptions], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("executeOptions") + @ExcludeMissing + fun _executeOptions(): JsonField = executeOptions + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("frameId") @ExcludeMissing fun _frameId(): JsonField = frameId + + /** + * Returns the raw JSON value of [shouldCache]. + * + * Unlike [shouldCache], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("shouldCache") + @ExcludeMissing + fun _shouldCache(): JsonField = shouldCache + + @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]. + * + * The following fields are required: + * ```java + * .agentConfig() + * .executeOptions() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Body]. */ + class Builder internal constructor() { + + private var agentConfig: JsonField? = null + private var executeOptions: JsonField? = null + private var frameId: JsonField = JsonMissing.of() + private var shouldCache: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(body: Body) = apply { + agentConfig = body.agentConfig + executeOptions = body.executeOptions + frameId = body.frameId + shouldCache = body.shouldCache + additionalProperties = body.additionalProperties.toMutableMap() + } + + fun agentConfig(agentConfig: AgentConfig) = agentConfig(JsonField.of(agentConfig)) + + /** + * Sets [Builder.agentConfig] to an arbitrary JSON value. + * + * You should usually call [Builder.agentConfig] with a well-typed [AgentConfig] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun agentConfig(agentConfig: JsonField) = apply { + this.agentConfig = agentConfig + } + + fun executeOptions(executeOptions: ExecuteOptions) = + executeOptions(JsonField.of(executeOptions)) + + /** + * Sets [Builder.executeOptions] to an arbitrary JSON value. + * + * You should usually call [Builder.executeOptions] with a well-typed [ExecuteOptions] + * value instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun executeOptions(executeOptions: JsonField) = apply { + this.executeOptions = executeOptions + } + + /** Target frame ID for the agent */ + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun frameId(frameId: JsonField) = apply { this.frameId = frameId } + + /** If true, the server captures a cache entry and returns it to the client */ + fun shouldCache(shouldCache: Boolean) = shouldCache(JsonField.of(shouldCache)) + + /** + * Sets [Builder.shouldCache] to an arbitrary JSON value. + * + * You should usually call [Builder.shouldCache] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun shouldCache(shouldCache: JsonField) = apply { + this.shouldCache = shouldCache + } + + 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. + * + * The following fields are required: + * ```java + * .agentConfig() + * .executeOptions() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Body = + Body( + checkRequired("agentConfig", agentConfig), + checkRequired("executeOptions", executeOptions), + frameId, + shouldCache, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Body = apply { + if (validated) { + return@apply + } + + agentConfig().validate() + executeOptions().validate() + frameId() + shouldCache() + 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 = + (agentConfig.asKnown().getOrNull()?.validity() ?: 0) + + (executeOptions.asKnown().getOrNull()?.validity() ?: 0) + + (if (frameId.asKnown().isPresent) 1 else 0) + + (if (shouldCache.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Body && + agentConfig == other.agentConfig && + executeOptions == other.executeOptions && + frameId == other.frameId && + shouldCache == other.shouldCache && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(agentConfig, executeOptions, frameId, shouldCache, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Body{agentConfig=$agentConfig, executeOptions=$executeOptions, frameId=$frameId, shouldCache=$shouldCache, additionalProperties=$additionalProperties}" + } + + class AgentConfig + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val cua: JsonField, + private val executionModel: JsonField, + private val mode: JsonField, + private val model: JsonField, + private val provider: JsonField, + private val systemPrompt: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("cua") @ExcludeMissing cua: JsonField = JsonMissing.of(), + @JsonProperty("executionModel") + @ExcludeMissing + executionModel: JsonField = JsonMissing.of(), + @JsonProperty("mode") @ExcludeMissing mode: JsonField = JsonMissing.of(), + @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), + @JsonProperty("provider") + @ExcludeMissing + provider: JsonField = JsonMissing.of(), + @JsonProperty("systemPrompt") + @ExcludeMissing + systemPrompt: JsonField = JsonMissing.of(), + ) : this(cua, executionModel, mode, model, provider, systemPrompt, mutableMapOf()) + + /** + * Deprecated. Use mode: 'cua' instead. If both are provided, mode takes precedence. + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun cua(): Optional = cua.getOptional("cua") + + /** + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') for tool + * execution (observe/act calls within agent tools). If not specified, inherits from the + * main model configuration. + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun executionModel(): Optional = + executionModel.getOptional("executionModel") + + /** + * Tool mode for the agent (dom, hybrid, cua). If set, overrides cua. + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun mode(): Optional = mode.getOptional("mode") + + /** + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun model(): Optional = model.getOptional("model") + + /** + * AI provider for the agent (legacy, use model: openai/gpt-5-nano instead) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun provider(): Optional = provider.getOptional("provider") + + /** + * Custom system prompt for the agent + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun systemPrompt(): Optional = systemPrompt.getOptional("systemPrompt") + + /** + * Returns the raw JSON value of [cua]. + * + * Unlike [cua], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("cua") @ExcludeMissing fun _cua(): JsonField = cua + + /** + * Returns the raw JSON value of [executionModel]. + * + * Unlike [executionModel], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("executionModel") + @ExcludeMissing + fun _executionModel(): JsonField = executionModel + + /** + * Returns the raw JSON value of [mode]. + * + * Unlike [mode], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("mode") @ExcludeMissing fun _mode(): JsonField = mode + + /** + * Returns the raw JSON value of [model]. + * + * Unlike [model], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model + + /** + * Returns the raw JSON value of [provider]. + * + * Unlike [provider], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("provider") @ExcludeMissing fun _provider(): JsonField = provider + + /** + * Returns the raw JSON value of [systemPrompt]. + * + * Unlike [systemPrompt], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("systemPrompt") + @ExcludeMissing + fun _systemPrompt(): JsonField = systemPrompt + + @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 [AgentConfig]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [AgentConfig]. */ + class Builder internal constructor() { + + private var cua: JsonField = JsonMissing.of() + private var executionModel: JsonField = JsonMissing.of() + private var mode: JsonField = JsonMissing.of() + private var model: JsonField = JsonMissing.of() + private var provider: JsonField = JsonMissing.of() + private var systemPrompt: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(agentConfig: AgentConfig) = apply { + cua = agentConfig.cua + executionModel = agentConfig.executionModel + mode = agentConfig.mode + model = agentConfig.model + provider = agentConfig.provider + systemPrompt = agentConfig.systemPrompt + additionalProperties = agentConfig.additionalProperties.toMutableMap() + } + + /** Deprecated. Use mode: 'cua' instead. If both are provided, mode takes precedence. */ + fun cua(cua: Boolean) = cua(JsonField.of(cua)) + + /** + * Sets [Builder.cua] to an arbitrary JSON value. + * + * You should usually call [Builder.cua] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun cua(cua: JsonField) = apply { this.cua = cua } + + /** + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') for tool + * execution (observe/act calls within agent tools). If not specified, inherits from the + * main model configuration. + */ + fun executionModel(executionModel: ExecutionModel) = + executionModel(JsonField.of(executionModel)) + + /** + * Sets [Builder.executionModel] to an arbitrary JSON value. + * + * You should usually call [Builder.executionModel] with a well-typed [ExecutionModel] + * value instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun executionModel(executionModel: JsonField) = apply { + this.executionModel = executionModel + } + + /** + * Alias for calling [executionModel] with `ExecutionModel.ofModelConfig(modelConfig)`. + */ + fun executionModel(modelConfig: ModelConfig) = + executionModel(ExecutionModel.ofModelConfig(modelConfig)) + + /** Alias for calling [executionModel] with `ExecutionModel.ofString(string)`. */ + fun executionModel(string: String) = executionModel(ExecutionModel.ofString(string)) + + /** Tool mode for the agent (dom, hybrid, cua). If set, overrides cua. */ + fun mode(mode: Mode) = mode(JsonField.of(mode)) + + /** + * Sets [Builder.mode] to an arbitrary JSON value. + * + * You should usually call [Builder.mode] with a well-typed [Mode] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun mode(mode: JsonField) = apply { this.mode = mode } + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + fun model(model: Model) = model(JsonField.of(model)) + + /** + * Sets [Builder.model] to an arbitrary JSON value. + * + * You should usually call [Builder.model] with a well-typed [Model] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun model(model: JsonField) = apply { this.model = model } + + /** Alias for calling [model] with `Model.ofConfig(config)`. */ + fun model(config: ModelConfig) = model(Model.ofConfig(config)) + + /** Alias for calling [model] with `Model.ofString(string)`. */ + fun model(string: String) = model(Model.ofString(string)) + + /** AI provider for the agent (legacy, use model: openai/gpt-5-nano instead) */ + fun provider(provider: Provider) = provider(JsonField.of(provider)) + + /** + * Sets [Builder.provider] to an arbitrary JSON value. + * + * You should usually call [Builder.provider] with a well-typed [Provider] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun provider(provider: JsonField) = apply { this.provider = provider } + + /** Custom system prompt for the agent */ + fun systemPrompt(systemPrompt: String) = systemPrompt(JsonField.of(systemPrompt)) + + /** + * Sets [Builder.systemPrompt] to an arbitrary JSON value. + * + * You should usually call [Builder.systemPrompt] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun systemPrompt(systemPrompt: JsonField) = apply { + this.systemPrompt = systemPrompt + } + + 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 [AgentConfig]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): AgentConfig = + AgentConfig( + cua, + executionModel, + mode, + model, + provider, + systemPrompt, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): AgentConfig = apply { + if (validated) { + return@apply + } + + cua() + executionModel().ifPresent { it.validate() } + mode().ifPresent { it.validate() } + model().ifPresent { it.validate() } + provider().ifPresent { it.validate() } + systemPrompt() + 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 = + (if (cua.asKnown().isPresent) 1 else 0) + + (executionModel.asKnown().getOrNull()?.validity() ?: 0) + + (mode.asKnown().getOrNull()?.validity() ?: 0) + + (model.asKnown().getOrNull()?.validity() ?: 0) + + (provider.asKnown().getOrNull()?.validity() ?: 0) + + (if (systemPrompt.asKnown().isPresent) 1 else 0) + + /** + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') for tool + * execution (observe/act calls within agent tools). If not specified, inherits from the + * main model configuration. + */ + @JsonDeserialize(using = ExecutionModel.Deserializer::class) + @JsonSerialize(using = ExecutionModel.Serializer::class) + class ExecutionModel + private constructor( + private val modelConfig: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun modelConfig(): Optional = Optional.ofNullable(modelConfig) + + fun string(): Optional = Optional.ofNullable(string) + + fun isModelConfig(): Boolean = modelConfig != null + + fun isString(): Boolean = string != null + + fun asModelConfig(): ModelConfig = modelConfig.getOrThrow("modelConfig") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given + * [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, + * unless [visitor] overrides [Visitor.unknown]. To handle variants not known to this + * version of the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = executionModel.accept(new ExecutionModel.Visitor>() { + * @Override + * public Optional visitModelConfig(ModelConfig modelConfig) { + * return Optional.of(modelConfig.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in + * [visitor] and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + modelConfig != null -> visitor.visitModelConfig(modelConfig) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): ExecutionModel = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitModelConfig(modelConfig: ModelConfig) { + modelConfig.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitModelConfig(modelConfig: ModelConfig) = + modelConfig.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is ExecutionModel && + modelConfig == other.modelConfig && + string == other.string + } + + override fun hashCode(): Int = Objects.hash(modelConfig, string) + + override fun toString(): String = + when { + modelConfig != null -> "ExecutionModel{modelConfig=$modelConfig}" + string != null -> "ExecutionModel{string=$string}" + _json != null -> "ExecutionModel{_unknown=$_json}" + else -> throw IllegalStateException("Invalid ExecutionModel") + } + + companion object { + + @JvmStatic + fun ofModelConfig(modelConfig: ModelConfig) = + ExecutionModel(modelConfig = modelConfig) + + @JvmStatic fun ofString(string: String) = ExecutionModel(string = string) + } + + /** + * An interface that defines how to map each variant of [ExecutionModel] to a value of + * type [T]. + */ + interface Visitor { + + fun visitModelConfig(modelConfig: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [ExecutionModel] to a value of type [T]. + * + * An instance of [ExecutionModel] can contain an unknown variant if it was + * deserialized from data that doesn't match any known variant. For example, if the + * SDK is on an older version than the API, then the API may respond with new + * variants that the SDK is unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown ExecutionModel: $json") + } + } + + internal class Deserializer : BaseDeserializer(ExecutionModel::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): ExecutionModel { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + ExecutionModel(modelConfig = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + ExecutionModel(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> ExecutionModel(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(ExecutionModel::class) { + + override fun serialize( + value: ExecutionModel, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.modelConfig != null -> generator.writeObject(value.modelConfig) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid ExecutionModel") + } + } + } + } + + /** Tool mode for the agent (dom, hybrid, cua). If set, overrides cua. */ + class Mode @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is + * on an older version than the API, then the API may respond with new members that the + * SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val DOM = of("dom") + + @JvmField val HYBRID = of("hybrid") + + @JvmField val CUA = of("cua") + + @JvmStatic fun of(value: String) = Mode(JsonField.of(value)) + } + + /** An enum containing [Mode]'s known values. */ + enum class Known { + DOM, + HYBRID, + CUA, + } + + /** + * An enum containing [Mode]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Mode] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + DOM, + HYBRID, + CUA, + /** An enum member indicating that [Mode] was instantiated with an unknown value. */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you + * want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + DOM -> Value.DOM + HYBRID -> Value.HYBRID + CUA -> Value.CUA + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + DOM -> Known.DOM + HYBRID -> Known.HYBRID + CUA -> Known.CUA + else -> throw StagehandInvalidDataException("Unknown Mode: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have + * the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Mode = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Mode && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + @JsonDeserialize(using = Model.Deserializer::class) + @JsonSerialize(using = Model.Serializer::class) + class Model + private constructor( + private val config: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun config(): Optional = Optional.ofNullable(config) + + fun string(): Optional = Optional.ofNullable(string) + + fun isConfig(): Boolean = config != null + + fun isString(): Boolean = string != null + + fun asConfig(): ModelConfig = config.getOrThrow("config") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given + * [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, + * unless [visitor] overrides [Visitor.unknown]. To handle variants not known to this + * version of the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = model.accept(new Model.Visitor>() { + * @Override + * public Optional visitConfig(ModelConfig config) { + * return Optional.of(config.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in + * [visitor] and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + config != null -> visitor.visitConfig(config) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Model = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) { + config.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) = config.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Model && config == other.config && string == other.string + } + + override fun hashCode(): Int = Objects.hash(config, string) + + override fun toString(): String = + when { + config != null -> "Model{config=$config}" + string != null -> "Model{string=$string}" + _json != null -> "Model{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Model") + } + + companion object { + + @JvmStatic fun ofConfig(config: ModelConfig) = Model(config = config) + + @JvmStatic fun ofString(string: String) = Model(string = string) + } + + /** + * An interface that defines how to map each variant of [Model] to a value of type [T]. + */ + interface Visitor { + + fun visitConfig(config: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [Model] to a value of type [T]. + * + * An instance of [Model] can contain an unknown variant if it was deserialized from + * data that doesn't match any known variant. For example, if the SDK is on an older + * version than the API, then the API may respond with new variants that the SDK is + * unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Model: $json") + } + } + + internal class Deserializer : BaseDeserializer(Model::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Model { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Model(config = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + Model(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> Model(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Model::class) { + + override fun serialize( + value: Model, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.config != null -> generator.writeObject(value.config) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Model") + } + } + } + } + + /** AI provider for the agent (legacy, use model: openai/gpt-5-nano instead) */ + class Provider @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is + * on an older version than the API, then the API may respond with new members that the + * SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val OPENAI = of("openai") + + @JvmField val ANTHROPIC = of("anthropic") + + @JvmField val GOOGLE = of("google") + + @JvmField val MICROSOFT = of("microsoft") + + @JvmField val BEDROCK = of("bedrock") + + @JvmStatic fun of(value: String) = Provider(JsonField.of(value)) + } + + /** An enum containing [Provider]'s known values. */ + enum class Known { + OPENAI, + ANTHROPIC, + GOOGLE, + MICROSOFT, + BEDROCK, + } + + /** + * An enum containing [Provider]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Provider] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + OPENAI, + ANTHROPIC, + GOOGLE, + MICROSOFT, + BEDROCK, + /** + * An enum member indicating that [Provider] was instantiated with an unknown value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you + * want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + OPENAI -> Value.OPENAI + ANTHROPIC -> Value.ANTHROPIC + GOOGLE -> Value.GOOGLE + MICROSOFT -> Value.MICROSOFT + BEDROCK -> Value.BEDROCK + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + OPENAI -> Known.OPENAI + ANTHROPIC -> Known.ANTHROPIC + GOOGLE -> Known.GOOGLE + MICROSOFT -> Known.MICROSOFT + BEDROCK -> Known.BEDROCK + else -> throw StagehandInvalidDataException("Unknown Provider: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have + * the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Provider = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Provider && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is AgentConfig && + cua == other.cua && + executionModel == other.executionModel && + mode == other.mode && + model == other.model && + provider == other.provider && + systemPrompt == other.systemPrompt && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + cua, + executionModel, + mode, + model, + provider, + systemPrompt, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "AgentConfig{cua=$cua, executionModel=$executionModel, mode=$mode, model=$model, provider=$provider, systemPrompt=$systemPrompt, additionalProperties=$additionalProperties}" + } + + class ExecuteOptions + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val instruction: JsonField, + private val highlightCursor: JsonField, + private val maxSteps: JsonField, + private val toolTimeout: JsonField, + private val useSearch: JsonField, + private val variables: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("instruction") + @ExcludeMissing + instruction: JsonField = JsonMissing.of(), + @JsonProperty("highlightCursor") + @ExcludeMissing + highlightCursor: JsonField = JsonMissing.of(), + @JsonProperty("maxSteps") + @ExcludeMissing + maxSteps: JsonField = JsonMissing.of(), + @JsonProperty("toolTimeout") + @ExcludeMissing + toolTimeout: JsonField = JsonMissing.of(), + @JsonProperty("useSearch") + @ExcludeMissing + useSearch: JsonField = JsonMissing.of(), + @JsonProperty("variables") + @ExcludeMissing + variables: JsonField = JsonMissing.of(), + ) : this( + instruction, + highlightCursor, + maxSteps, + toolTimeout, + useSearch, + variables, + mutableMapOf(), + ) + + /** + * Natural language instruction for the agent + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun instruction(): String = instruction.getRequired("instruction") + + /** + * Whether to visually highlight the cursor during execution + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun highlightCursor(): Optional = highlightCursor.getOptional("highlightCursor") + + /** + * Maximum number of steps the agent can take + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun maxSteps(): Optional = maxSteps.getOptional("maxSteps") + + /** + * Timeout in milliseconds for each agent tool call + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun toolTimeout(): Optional = toolTimeout.getOptional("toolTimeout") + + /** + * Whether to enable the web search tool powered by Browserbase Search API + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun useSearch(): Optional = useSearch.getOptional("useSearch") + + /** + * Variables available to the agent via %variableName% syntax in supported tools + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun variables(): Optional = variables.getOptional("variables") + + /** + * Returns the raw JSON value of [instruction]. + * + * Unlike [instruction], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("instruction") + @ExcludeMissing + fun _instruction(): JsonField = instruction + + /** + * Returns the raw JSON value of [highlightCursor]. + * + * Unlike [highlightCursor], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("highlightCursor") + @ExcludeMissing + fun _highlightCursor(): JsonField = highlightCursor + + /** + * Returns the raw JSON value of [maxSteps]. + * + * Unlike [maxSteps], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("maxSteps") @ExcludeMissing fun _maxSteps(): JsonField = maxSteps + + /** + * Returns the raw JSON value of [toolTimeout]. + * + * Unlike [toolTimeout], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("toolTimeout") + @ExcludeMissing + fun _toolTimeout(): JsonField = toolTimeout + + /** + * Returns the raw JSON value of [useSearch]. + * + * Unlike [useSearch], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("useSearch") @ExcludeMissing fun _useSearch(): JsonField = useSearch + + /** + * Returns the raw JSON value of [variables]. + * + * Unlike [variables], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("variables") + @ExcludeMissing + fun _variables(): JsonField = variables + + @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 [ExecuteOptions]. + * + * The following fields are required: + * ```java + * .instruction() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [ExecuteOptions]. */ + class Builder internal constructor() { + + private var instruction: JsonField? = null + private var highlightCursor: JsonField = JsonMissing.of() + private var maxSteps: JsonField = JsonMissing.of() + private var toolTimeout: JsonField = JsonMissing.of() + private var useSearch: JsonField = JsonMissing.of() + private var variables: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(executeOptions: ExecuteOptions) = apply { + instruction = executeOptions.instruction + highlightCursor = executeOptions.highlightCursor + maxSteps = executeOptions.maxSteps + toolTimeout = executeOptions.toolTimeout + useSearch = executeOptions.useSearch + variables = executeOptions.variables + additionalProperties = executeOptions.additionalProperties.toMutableMap() + } + + /** Natural language instruction for the agent */ + fun instruction(instruction: String) = instruction(JsonField.of(instruction)) + + /** + * Sets [Builder.instruction] to an arbitrary JSON value. + * + * You should usually call [Builder.instruction] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun instruction(instruction: JsonField) = apply { + this.instruction = instruction + } + + /** Whether to visually highlight the cursor during execution */ + fun highlightCursor(highlightCursor: Boolean) = + highlightCursor(JsonField.of(highlightCursor)) + + /** + * Sets [Builder.highlightCursor] to an arbitrary JSON value. + * + * You should usually call [Builder.highlightCursor] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun highlightCursor(highlightCursor: JsonField) = apply { + this.highlightCursor = highlightCursor + } + + /** Maximum number of steps the agent can take */ + fun maxSteps(maxSteps: Double) = maxSteps(JsonField.of(maxSteps)) + + /** + * Sets [Builder.maxSteps] to an arbitrary JSON value. + * + * You should usually call [Builder.maxSteps] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun maxSteps(maxSteps: JsonField) = apply { this.maxSteps = maxSteps } + + /** Timeout in milliseconds for each agent tool call */ + fun toolTimeout(toolTimeout: Double) = toolTimeout(JsonField.of(toolTimeout)) + + /** + * Sets [Builder.toolTimeout] to an arbitrary JSON value. + * + * You should usually call [Builder.toolTimeout] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun toolTimeout(toolTimeout: JsonField) = apply { + this.toolTimeout = toolTimeout + } + + /** Whether to enable the web search tool powered by Browserbase Search API */ + fun useSearch(useSearch: Boolean) = useSearch(JsonField.of(useSearch)) + + /** + * Sets [Builder.useSearch] to an arbitrary JSON value. + * + * You should usually call [Builder.useSearch] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun useSearch(useSearch: JsonField) = apply { this.useSearch = useSearch } + + /** Variables available to the agent via %variableName% syntax in supported tools */ + fun variables(variables: Variables) = variables(JsonField.of(variables)) + + /** + * Sets [Builder.variables] to an arbitrary JSON value. + * + * You should usually call [Builder.variables] with a well-typed [Variables] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun variables(variables: JsonField) = apply { this.variables = variables } + + 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 [ExecuteOptions]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .instruction() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): ExecuteOptions = + ExecuteOptions( + checkRequired("instruction", instruction), + highlightCursor, + maxSteps, + toolTimeout, + useSearch, + variables, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): ExecuteOptions = apply { + if (validated) { + return@apply + } + + instruction() + highlightCursor() + maxSteps() + toolTimeout() + useSearch() + variables().ifPresent { it.validate() } + 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 = + (if (instruction.asKnown().isPresent) 1 else 0) + + (if (highlightCursor.asKnown().isPresent) 1 else 0) + + (if (maxSteps.asKnown().isPresent) 1 else 0) + + (if (toolTimeout.asKnown().isPresent) 1 else 0) + + (if (useSearch.asKnown().isPresent) 1 else 0) + + (variables.asKnown().getOrNull()?.validity() ?: 0) + + /** Variables available to the agent via %variableName% syntax in supported tools */ + class Variables + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Variables]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Variables]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(variables: Variables) = apply { + additionalProperties = variables.additionalProperties.toMutableMap() + } + + 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 [Variables]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Variables = Variables(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Variables = 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 = + additionalProperties.count { (_, value) -> !value.isNull() && !value.isMissing() } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Variables && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Variables{additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is ExecuteOptions && + instruction == other.instruction && + highlightCursor == other.highlightCursor && + maxSteps == other.maxSteps && + toolTimeout == other.toolTimeout && + useSearch == other.useSearch && + variables == other.variables && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + instruction, + highlightCursor, + maxSteps, + toolTimeout, + useSearch, + variables, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "ExecuteOptions{instruction=$instruction, highlightCursor=$highlightCursor, maxSteps=$maxSteps, toolTimeout=$toolTimeout, useSearch=$useSearch, variables=$variables, additionalProperties=$additionalProperties}" + } + + /** Whether to stream the response via SSE */ + class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val TRUE = of("true") + + @JvmField val FALSE = of("false") + + @JvmStatic fun of(value: String) = XStreamResponse(JsonField.of(value)) + } + + /** An enum containing [XStreamResponse]'s known values. */ + enum class Known { + TRUE, + FALSE, + } + + /** + * An enum containing [XStreamResponse]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [XStreamResponse] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + TRUE, + FALSE, + /** + * An enum member indicating that [XStreamResponse] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + TRUE -> Value.TRUE + FALSE -> Value.FALSE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + TRUE -> Known.TRUE + FALSE -> Known.FALSE + else -> throw StagehandInvalidDataException("Unknown XStreamResponse: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): XStreamResponse = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is XStreamResponse && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionExecuteParams && + id == other.id && + xStreamResponse == other.xStreamResponse && + body == other.body && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams + } + + override fun hashCode(): Int = + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) + + override fun toString() = + "SessionExecuteParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponse.kt new file mode 100644 index 0000000..c2837fc --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponse.kt @@ -0,0 +1,1960 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +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.checkKnown +import com.browserbase.api.core.checkRequired +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class SessionExecuteResponse +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val data: JsonField, + private val success: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("success") @ExcludeMissing success: JsonField = JsonMissing.of(), + ) : this(data, success, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun data(): Data = data.getRequired("data") + + /** + * Indicates whether the request was successful + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [data]. + * + * Unlike [data], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("data") @ExcludeMissing fun _data(): JsonField = data + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @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 [SessionExecuteResponse]. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionExecuteResponse]. */ + class Builder internal constructor() { + + private var data: JsonField? = null + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(sessionExecuteResponse: SessionExecuteResponse) = apply { + data = sessionExecuteResponse.data + success = sessionExecuteResponse.success + additionalProperties = sessionExecuteResponse.additionalProperties.toMutableMap() + } + + fun data(data: Data) = data(JsonField.of(data)) + + /** + * Sets [Builder.data] to an arbitrary JSON value. + * + * You should usually call [Builder.data] with a well-typed [Data] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun data(data: JsonField) = apply { this.data = data } + + /** Indicates whether the request was successful */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [SessionExecuteResponse]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionExecuteResponse = + SessionExecuteResponse( + checkRequired("data", data), + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): SessionExecuteResponse = apply { + if (validated) { + return@apply + } + + data().validate() + success() + 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 = + (data.asKnown().getOrNull()?.validity() ?: 0) + (if (success.asKnown().isPresent) 1 else 0) + + class Data + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val result: JsonField, + private val cacheEntry: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("result") @ExcludeMissing result: JsonField = JsonMissing.of(), + @JsonProperty("cacheEntry") + @ExcludeMissing + cacheEntry: JsonField = JsonMissing.of(), + ) : this(result, cacheEntry, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun result(): Result = result.getRequired("result") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun cacheEntry(): Optional = cacheEntry.getOptional("cacheEntry") + + /** + * Returns the raw JSON value of [result]. + * + * Unlike [result], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("result") @ExcludeMissing fun _result(): JsonField = result + + /** + * Returns the raw JSON value of [cacheEntry]. + * + * Unlike [cacheEntry], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("cacheEntry") + @ExcludeMissing + fun _cacheEntry(): JsonField = cacheEntry + + @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 [Data]. + * + * The following fields are required: + * ```java + * .result() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Data]. */ + class Builder internal constructor() { + + private var result: JsonField? = null + private var cacheEntry: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(data: Data) = apply { + result = data.result + cacheEntry = data.cacheEntry + additionalProperties = data.additionalProperties.toMutableMap() + } + + fun result(result: Result) = result(JsonField.of(result)) + + /** + * Sets [Builder.result] to an arbitrary JSON value. + * + * You should usually call [Builder.result] with a well-typed [Result] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun result(result: JsonField) = apply { this.result = result } + + fun cacheEntry(cacheEntry: CacheEntry) = cacheEntry(JsonField.of(cacheEntry)) + + /** + * Sets [Builder.cacheEntry] to an arbitrary JSON value. + * + * You should usually call [Builder.cacheEntry] with a well-typed [CacheEntry] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun cacheEntry(cacheEntry: JsonField) = apply { + this.cacheEntry = cacheEntry + } + + 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 [Data]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .result() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Data = + Data( + checkRequired("result", result), + cacheEntry, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Data = apply { + if (validated) { + return@apply + } + + result().validate() + cacheEntry().ifPresent { it.validate() } + 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 = + (result.asKnown().getOrNull()?.validity() ?: 0) + + (cacheEntry.asKnown().getOrNull()?.validity() ?: 0) + + class Result + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val actions: JsonField>, + private val completed: JsonField, + private val message: JsonField, + private val success: JsonField, + private val metadata: JsonField, + private val usage: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("actions") + @ExcludeMissing + actions: JsonField> = JsonMissing.of(), + @JsonProperty("completed") + @ExcludeMissing + completed: JsonField = JsonMissing.of(), + @JsonProperty("message") + @ExcludeMissing + message: JsonField = JsonMissing.of(), + @JsonProperty("success") + @ExcludeMissing + success: JsonField = JsonMissing.of(), + @JsonProperty("metadata") + @ExcludeMissing + metadata: JsonField = JsonMissing.of(), + @JsonProperty("usage") @ExcludeMissing usage: JsonField = JsonMissing.of(), + ) : this(actions, completed, message, success, metadata, usage, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun actions(): List = actions.getRequired("actions") + + /** + * Whether the agent finished its task + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun completed(): Boolean = completed.getRequired("completed") + + /** + * Summary of what the agent accomplished + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun message(): String = message.getRequired("message") + + /** + * Whether the agent completed successfully + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun metadata(): Optional = metadata.getOptional("metadata") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun usage(): Optional = usage.getOptional("usage") + + /** + * Returns the raw JSON value of [actions]. + * + * Unlike [actions], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("actions") + @ExcludeMissing + fun _actions(): JsonField> = actions + + /** + * Returns the raw JSON value of [completed]. + * + * Unlike [completed], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("completed") + @ExcludeMissing + fun _completed(): JsonField = completed + + /** + * Returns the raw JSON value of [message]. + * + * Unlike [message], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("message") @ExcludeMissing fun _message(): JsonField = message + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + /** + * Returns the raw JSON value of [metadata]. + * + * Unlike [metadata], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("metadata") + @ExcludeMissing + fun _metadata(): JsonField = metadata + + /** + * Returns the raw JSON value of [usage]. + * + * Unlike [usage], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("usage") @ExcludeMissing fun _usage(): JsonField = usage + + @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 [Result]. + * + * The following fields are required: + * ```java + * .actions() + * .completed() + * .message() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Result]. */ + class Builder internal constructor() { + + private var actions: JsonField>? = null + private var completed: JsonField? = null + private var message: JsonField? = null + private var success: JsonField? = null + private var metadata: JsonField = JsonMissing.of() + private var usage: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(result: Result) = apply { + actions = result.actions.map { it.toMutableList() } + completed = result.completed + message = result.message + success = result.success + metadata = result.metadata + usage = result.usage + additionalProperties = result.additionalProperties.toMutableMap() + } + + fun actions(actions: List) = actions(JsonField.of(actions)) + + /** + * Sets [Builder.actions] to an arbitrary JSON value. + * + * You should usually call [Builder.actions] with a well-typed `List` value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun actions(actions: JsonField>) = apply { + this.actions = actions.map { it.toMutableList() } + } + + /** + * Adds a single [Action] to [actions]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addAction(action: Action) = apply { + actions = + (actions ?: JsonField.of(mutableListOf())).also { + checkKnown("actions", it).add(action) + } + } + + /** Whether the agent finished its task */ + fun completed(completed: Boolean) = completed(JsonField.of(completed)) + + /** + * Sets [Builder.completed] to an arbitrary JSON value. + * + * You should usually call [Builder.completed] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun completed(completed: JsonField) = apply { this.completed = completed } + + /** Summary of what the agent accomplished */ + fun message(message: String) = message(JsonField.of(message)) + + /** + * Sets [Builder.message] to an arbitrary JSON value. + * + * You should usually call [Builder.message] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun message(message: JsonField) = apply { this.message = message } + + /** Whether the agent completed successfully */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + fun metadata(metadata: Metadata) = metadata(JsonField.of(metadata)) + + /** + * Sets [Builder.metadata] to an arbitrary JSON value. + * + * You should usually call [Builder.metadata] with a well-typed [Metadata] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun metadata(metadata: JsonField) = apply { this.metadata = metadata } + + fun usage(usage: Usage) = usage(JsonField.of(usage)) + + /** + * Sets [Builder.usage] to an arbitrary JSON value. + * + * You should usually call [Builder.usage] with a well-typed [Usage] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun usage(usage: JsonField) = apply { this.usage = usage } + + 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 [Result]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .actions() + * .completed() + * .message() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Result = + Result( + checkRequired("actions", actions).map { it.toImmutable() }, + checkRequired("completed", completed), + checkRequired("message", message), + checkRequired("success", success), + metadata, + usage, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Result = apply { + if (validated) { + return@apply + } + + actions().forEach { it.validate() } + completed() + message() + success() + metadata().ifPresent { it.validate() } + usage().ifPresent { it.validate() } + 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 = + (actions.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + (if (completed.asKnown().isPresent) 1 else 0) + + (if (message.asKnown().isPresent) 1 else 0) + + (if (success.asKnown().isPresent) 1 else 0) + + (metadata.asKnown().getOrNull()?.validity() ?: 0) + + (usage.asKnown().getOrNull()?.validity() ?: 0) + + class Action + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val type: JsonField, + private val action: JsonField, + private val instruction: JsonField, + private val pageText: JsonField, + private val pageUrl: JsonField, + private val reasoning: JsonField, + private val taskCompleted: JsonField, + private val timeMs: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("type") + @ExcludeMissing + type: JsonField = JsonMissing.of(), + @JsonProperty("action") + @ExcludeMissing + action: JsonField = JsonMissing.of(), + @JsonProperty("instruction") + @ExcludeMissing + instruction: JsonField = JsonMissing.of(), + @JsonProperty("pageText") + @ExcludeMissing + pageText: JsonField = JsonMissing.of(), + @JsonProperty("pageUrl") + @ExcludeMissing + pageUrl: JsonField = JsonMissing.of(), + @JsonProperty("reasoning") + @ExcludeMissing + reasoning: JsonField = JsonMissing.of(), + @JsonProperty("taskCompleted") + @ExcludeMissing + taskCompleted: JsonField = JsonMissing.of(), + @JsonProperty("timeMs") + @ExcludeMissing + timeMs: JsonField = JsonMissing.of(), + ) : this( + type, + action, + instruction, + pageText, + pageUrl, + reasoning, + taskCompleted, + timeMs, + mutableMapOf(), + ) + + /** + * Type of action taken + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun type(): String = type.getRequired("type") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun action(): Optional = action.getOptional("action") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun instruction(): Optional = instruction.getOptional("instruction") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun pageText(): Optional = pageText.getOptional("pageText") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun pageUrl(): Optional = pageUrl.getOptional("pageUrl") + + /** + * Agent's reasoning for taking this action + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun reasoning(): Optional = reasoning.getOptional("reasoning") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun taskCompleted(): Optional = taskCompleted.getOptional("taskCompleted") + + /** + * Time taken for this action in ms + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun timeMs(): Optional = timeMs.getOptional("timeMs") + + /** + * Returns the raw JSON value of [type]. + * + * Unlike [type], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("type") @ExcludeMissing fun _type(): JsonField = type + + /** + * Returns the raw JSON value of [action]. + * + * Unlike [action], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("action") @ExcludeMissing fun _action(): JsonField = action + + /** + * Returns the raw JSON value of [instruction]. + * + * Unlike [instruction], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("instruction") + @ExcludeMissing + fun _instruction(): JsonField = instruction + + /** + * Returns the raw JSON value of [pageText]. + * + * Unlike [pageText], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("pageText") + @ExcludeMissing + fun _pageText(): JsonField = pageText + + /** + * Returns the raw JSON value of [pageUrl]. + * + * Unlike [pageUrl], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("pageUrl") @ExcludeMissing fun _pageUrl(): JsonField = pageUrl + + /** + * Returns the raw JSON value of [reasoning]. + * + * Unlike [reasoning], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("reasoning") + @ExcludeMissing + fun _reasoning(): JsonField = reasoning + + /** + * Returns the raw JSON value of [taskCompleted]. + * + * Unlike [taskCompleted], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("taskCompleted") + @ExcludeMissing + fun _taskCompleted(): JsonField = taskCompleted + + /** + * Returns the raw JSON value of [timeMs]. + * + * Unlike [timeMs], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("timeMs") @ExcludeMissing fun _timeMs(): JsonField = timeMs + + @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 [Action]. + * + * The following fields are required: + * ```java + * .type() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Action]. */ + class Builder internal constructor() { + + private var type: JsonField? = null + private var action: JsonField = JsonMissing.of() + private var instruction: JsonField = JsonMissing.of() + private var pageText: JsonField = JsonMissing.of() + private var pageUrl: JsonField = JsonMissing.of() + private var reasoning: JsonField = JsonMissing.of() + private var taskCompleted: JsonField = JsonMissing.of() + private var timeMs: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(action: Action) = apply { + type = action.type + this.action = action.action + instruction = action.instruction + pageText = action.pageText + pageUrl = action.pageUrl + reasoning = action.reasoning + taskCompleted = action.taskCompleted + timeMs = action.timeMs + additionalProperties = action.additionalProperties.toMutableMap() + } + + /** Type of action taken */ + fun type(type: String) = type(JsonField.of(type)) + + /** + * Sets [Builder.type] to an arbitrary JSON value. + * + * You should usually call [Builder.type] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun type(type: JsonField) = apply { this.type = type } + + fun action(action: String) = action(JsonField.of(action)) + + /** + * Sets [Builder.action] to an arbitrary JSON value. + * + * You should usually call [Builder.action] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun action(action: JsonField) = apply { this.action = action } + + fun instruction(instruction: String) = instruction(JsonField.of(instruction)) + + /** + * Sets [Builder.instruction] to an arbitrary JSON value. + * + * You should usually call [Builder.instruction] with a well-typed [String] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun instruction(instruction: JsonField) = apply { + this.instruction = instruction + } + + fun pageText(pageText: String) = pageText(JsonField.of(pageText)) + + /** + * Sets [Builder.pageText] to an arbitrary JSON value. + * + * You should usually call [Builder.pageText] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun pageText(pageText: JsonField) = apply { this.pageText = pageText } + + fun pageUrl(pageUrl: String) = pageUrl(JsonField.of(pageUrl)) + + /** + * Sets [Builder.pageUrl] to an arbitrary JSON value. + * + * You should usually call [Builder.pageUrl] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun pageUrl(pageUrl: JsonField) = apply { this.pageUrl = pageUrl } + + /** Agent's reasoning for taking this action */ + fun reasoning(reasoning: String) = reasoning(JsonField.of(reasoning)) + + /** + * Sets [Builder.reasoning] to an arbitrary JSON value. + * + * You should usually call [Builder.reasoning] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun reasoning(reasoning: JsonField) = apply { + this.reasoning = reasoning + } + + fun taskCompleted(taskCompleted: Boolean) = + taskCompleted(JsonField.of(taskCompleted)) + + /** + * Sets [Builder.taskCompleted] to an arbitrary JSON value. + * + * You should usually call [Builder.taskCompleted] with a well-typed [Boolean] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun taskCompleted(taskCompleted: JsonField) = apply { + this.taskCompleted = taskCompleted + } + + /** Time taken for this action in ms */ + fun timeMs(timeMs: Double) = timeMs(JsonField.of(timeMs)) + + /** + * Sets [Builder.timeMs] to an arbitrary JSON value. + * + * You should usually call [Builder.timeMs] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun timeMs(timeMs: JsonField) = apply { this.timeMs = timeMs } + + 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 [Action]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .type() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Action = + Action( + checkRequired("type", type), + action, + instruction, + pageText, + pageUrl, + reasoning, + taskCompleted, + timeMs, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Action = apply { + if (validated) { + return@apply + } + + type() + action() + instruction() + pageText() + pageUrl() + reasoning() + taskCompleted() + timeMs() + 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 = + (if (type.asKnown().isPresent) 1 else 0) + + (if (action.asKnown().isPresent) 1 else 0) + + (if (instruction.asKnown().isPresent) 1 else 0) + + (if (pageText.asKnown().isPresent) 1 else 0) + + (if (pageUrl.asKnown().isPresent) 1 else 0) + + (if (reasoning.asKnown().isPresent) 1 else 0) + + (if (taskCompleted.asKnown().isPresent) 1 else 0) + + (if (timeMs.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Action && + type == other.type && + action == other.action && + instruction == other.instruction && + pageText == other.pageText && + pageUrl == other.pageUrl && + reasoning == other.reasoning && + taskCompleted == other.taskCompleted && + timeMs == other.timeMs && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + type, + action, + instruction, + pageText, + pageUrl, + reasoning, + taskCompleted, + timeMs, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Action{type=$type, action=$action, instruction=$instruction, pageText=$pageText, pageUrl=$pageUrl, reasoning=$reasoning, taskCompleted=$taskCompleted, timeMs=$timeMs, additionalProperties=$additionalProperties}" + } + + class Metadata + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Metadata]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Metadata]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(metadata: Metadata) = apply { + additionalProperties = metadata.additionalProperties.toMutableMap() + } + + 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 [Metadata]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Metadata = Metadata(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Metadata = 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 = + additionalProperties.count { (_, value) -> + !value.isNull() && !value.isMissing() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Metadata && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Metadata{additionalProperties=$additionalProperties}" + } + + class Usage + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val inferenceTimeMs: JsonField, + private val inputTokens: JsonField, + private val outputTokens: JsonField, + private val cachedInputTokens: JsonField, + private val reasoningTokens: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("inference_time_ms") + @ExcludeMissing + inferenceTimeMs: JsonField = JsonMissing.of(), + @JsonProperty("input_tokens") + @ExcludeMissing + inputTokens: JsonField = JsonMissing.of(), + @JsonProperty("output_tokens") + @ExcludeMissing + outputTokens: JsonField = JsonMissing.of(), + @JsonProperty("cached_input_tokens") + @ExcludeMissing + cachedInputTokens: JsonField = JsonMissing.of(), + @JsonProperty("reasoning_tokens") + @ExcludeMissing + reasoningTokens: JsonField = JsonMissing.of(), + ) : this( + inferenceTimeMs, + inputTokens, + outputTokens, + cachedInputTokens, + reasoningTokens, + mutableMapOf(), + ) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun inferenceTimeMs(): Double = inferenceTimeMs.getRequired("inference_time_ms") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun inputTokens(): Double = inputTokens.getRequired("input_tokens") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun outputTokens(): Double = outputTokens.getRequired("output_tokens") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun cachedInputTokens(): Optional = + cachedInputTokens.getOptional("cached_input_tokens") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun reasoningTokens(): Optional = + reasoningTokens.getOptional("reasoning_tokens") + + /** + * Returns the raw JSON value of [inferenceTimeMs]. + * + * Unlike [inferenceTimeMs], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("inference_time_ms") + @ExcludeMissing + fun _inferenceTimeMs(): JsonField = inferenceTimeMs + + /** + * Returns the raw JSON value of [inputTokens]. + * + * Unlike [inputTokens], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("input_tokens") + @ExcludeMissing + fun _inputTokens(): JsonField = inputTokens + + /** + * Returns the raw JSON value of [outputTokens]. + * + * Unlike [outputTokens], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("output_tokens") + @ExcludeMissing + fun _outputTokens(): JsonField = outputTokens + + /** + * Returns the raw JSON value of [cachedInputTokens]. + * + * Unlike [cachedInputTokens], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("cached_input_tokens") + @ExcludeMissing + fun _cachedInputTokens(): JsonField = cachedInputTokens + + /** + * Returns the raw JSON value of [reasoningTokens]. + * + * Unlike [reasoningTokens], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("reasoning_tokens") + @ExcludeMissing + fun _reasoningTokens(): JsonField = reasoningTokens + + @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 [Usage]. + * + * The following fields are required: + * ```java + * .inferenceTimeMs() + * .inputTokens() + * .outputTokens() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Usage]. */ + class Builder internal constructor() { + + private var inferenceTimeMs: JsonField? = null + private var inputTokens: JsonField? = null + private var outputTokens: JsonField? = null + private var cachedInputTokens: JsonField = JsonMissing.of() + private var reasoningTokens: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(usage: Usage) = apply { + inferenceTimeMs = usage.inferenceTimeMs + inputTokens = usage.inputTokens + outputTokens = usage.outputTokens + cachedInputTokens = usage.cachedInputTokens + reasoningTokens = usage.reasoningTokens + additionalProperties = usage.additionalProperties.toMutableMap() + } + + fun inferenceTimeMs(inferenceTimeMs: Double) = + inferenceTimeMs(JsonField.of(inferenceTimeMs)) + + /** + * Sets [Builder.inferenceTimeMs] to an arbitrary JSON value. + * + * You should usually call [Builder.inferenceTimeMs] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun inferenceTimeMs(inferenceTimeMs: JsonField) = apply { + this.inferenceTimeMs = inferenceTimeMs + } + + fun inputTokens(inputTokens: Double) = inputTokens(JsonField.of(inputTokens)) + + /** + * Sets [Builder.inputTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.inputTokens] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun inputTokens(inputTokens: JsonField) = apply { + this.inputTokens = inputTokens + } + + fun outputTokens(outputTokens: Double) = + outputTokens(JsonField.of(outputTokens)) + + /** + * Sets [Builder.outputTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.outputTokens] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun outputTokens(outputTokens: JsonField) = apply { + this.outputTokens = outputTokens + } + + fun cachedInputTokens(cachedInputTokens: Double) = + cachedInputTokens(JsonField.of(cachedInputTokens)) + + /** + * Sets [Builder.cachedInputTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.cachedInputTokens] with a well-typed + * [Double] value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun cachedInputTokens(cachedInputTokens: JsonField) = apply { + this.cachedInputTokens = cachedInputTokens + } + + fun reasoningTokens(reasoningTokens: Double) = + reasoningTokens(JsonField.of(reasoningTokens)) + + /** + * Sets [Builder.reasoningTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.reasoningTokens] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun reasoningTokens(reasoningTokens: JsonField) = apply { + this.reasoningTokens = reasoningTokens + } + + 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 [Usage]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .inferenceTimeMs() + * .inputTokens() + * .outputTokens() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Usage = + Usage( + checkRequired("inferenceTimeMs", inferenceTimeMs), + checkRequired("inputTokens", inputTokens), + checkRequired("outputTokens", outputTokens), + cachedInputTokens, + reasoningTokens, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Usage = apply { + if (validated) { + return@apply + } + + inferenceTimeMs() + inputTokens() + outputTokens() + cachedInputTokens() + reasoningTokens() + 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 = + (if (inferenceTimeMs.asKnown().isPresent) 1 else 0) + + (if (inputTokens.asKnown().isPresent) 1 else 0) + + (if (outputTokens.asKnown().isPresent) 1 else 0) + + (if (cachedInputTokens.asKnown().isPresent) 1 else 0) + + (if (reasoningTokens.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Usage && + inferenceTimeMs == other.inferenceTimeMs && + inputTokens == other.inputTokens && + outputTokens == other.outputTokens && + cachedInputTokens == other.cachedInputTokens && + reasoningTokens == other.reasoningTokens && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + inferenceTimeMs, + inputTokens, + outputTokens, + cachedInputTokens, + reasoningTokens, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Usage{inferenceTimeMs=$inferenceTimeMs, inputTokens=$inputTokens, outputTokens=$outputTokens, cachedInputTokens=$cachedInputTokens, reasoningTokens=$reasoningTokens, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Result && + actions == other.actions && + completed == other.completed && + message == other.message && + success == other.success && + metadata == other.metadata && + usage == other.usage && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + actions, + completed, + message, + success, + metadata, + usage, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Result{actions=$actions, completed=$completed, message=$message, success=$success, metadata=$metadata, usage=$usage, additionalProperties=$additionalProperties}" + } + + class CacheEntry + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val cacheKey: JsonField, + private val entry: JsonValue, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("cacheKey") + @ExcludeMissing + cacheKey: JsonField = JsonMissing.of(), + @JsonProperty("entry") @ExcludeMissing entry: JsonValue = JsonMissing.of(), + ) : this(cacheKey, entry, mutableMapOf()) + + /** + * Opaque cache identifier computed from instruction, URL, options, and config + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun cacheKey(): String = cacheKey.getRequired("cacheKey") + + /** + * Serialized cache entry that can be written to disk + * + * This arbitrary value can be deserialized into a custom type using the `convert` + * method: + * ```java + * MyClass myObject = cacheEntry.entry().convert(MyClass.class); + * ``` + */ + @JsonProperty("entry") @ExcludeMissing fun _entry(): JsonValue = entry + + /** + * Returns the raw JSON value of [cacheKey]. + * + * Unlike [cacheKey], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("cacheKey") @ExcludeMissing fun _cacheKey(): JsonField = cacheKey + + @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 [CacheEntry]. + * + * The following fields are required: + * ```java + * .cacheKey() + * .entry() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [CacheEntry]. */ + class Builder internal constructor() { + + private var cacheKey: JsonField? = null + private var entry: JsonValue? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(cacheEntry: CacheEntry) = apply { + cacheKey = cacheEntry.cacheKey + entry = cacheEntry.entry + additionalProperties = cacheEntry.additionalProperties.toMutableMap() + } + + /** Opaque cache identifier computed from instruction, URL, options, and config */ + fun cacheKey(cacheKey: String) = cacheKey(JsonField.of(cacheKey)) + + /** + * Sets [Builder.cacheKey] to an arbitrary JSON value. + * + * You should usually call [Builder.cacheKey] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun cacheKey(cacheKey: JsonField) = apply { this.cacheKey = cacheKey } + + /** Serialized cache entry that can be written to disk */ + fun entry(entry: JsonValue) = apply { this.entry = entry } + + 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 [CacheEntry]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .cacheKey() + * .entry() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): CacheEntry = + CacheEntry( + checkRequired("cacheKey", cacheKey), + checkRequired("entry", entry), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): CacheEntry = apply { + if (validated) { + return@apply + } + + cacheKey() + 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 = (if (cacheKey.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is CacheEntry && + cacheKey == other.cacheKey && + entry == other.entry && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(cacheKey, entry, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "CacheEntry{cacheKey=$cacheKey, entry=$entry, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Data && + result == other.result && + cacheEntry == other.cacheEntry && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(result, cacheEntry, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Data{result=$result, cacheEntry=$cacheEntry, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionExecuteResponse && + data == other.data && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(data, success, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "SessionExecuteResponse{data=$data, success=$success, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractParams.kt new file mode 100644 index 0000000..5c5fe92 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractParams.kt @@ -0,0 +1,1405 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.BaseDeserializer +import com.browserbase.api.core.BaseSerializer +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.allMaxBy +import com.browserbase.api.core.checkKnown +import com.browserbase.api.core.getOrThrow +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 com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import java.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** Extracts structured data from the current page using AI-powered analysis. */ +class SessionExtractParams +private constructor( + private val id: String?, + private val xStreamResponse: XStreamResponse?, + private val body: Body, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, +) : Params { + + /** Unique session identifier */ + fun id(): Optional = Optional.ofNullable(id) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) + + /** + * Target frame ID for the extraction + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun frameId(): Optional = body.frameId() + + /** + * Natural language instruction for what to extract + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun instruction(): Optional = body.instruction() + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun options(): Optional = body.options() + + /** + * JSON Schema defining the structure of data to extract + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun schema(): Optional = body.schema() + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _frameId(): JsonField = body._frameId() + + /** + * Returns the raw JSON value of [instruction]. + * + * Unlike [instruction], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _instruction(): JsonField = body._instruction() + + /** + * Returns the raw JSON value of [options]. + * + * Unlike [options], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _options(): JsonField = body._options() + + /** + * Returns the raw JSON value of [schema]. + * + * Unlike [schema], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _schema(): JsonField = body._schema() + + fun _additionalBodyProperties(): Map = body._additionalProperties() + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + @JvmStatic fun none(): SessionExtractParams = builder().build() + + /** Returns a mutable builder for constructing an instance of [SessionExtractParams]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionExtractParams]. */ + class Builder internal constructor() { + + private var id: String? = 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() + + @JvmSynthetic + internal fun from(sessionExtractParams: SessionExtractParams) = apply { + id = sessionExtractParams.id + xStreamResponse = sessionExtractParams.xStreamResponse + body = sessionExtractParams.body.toBuilder() + additionalHeaders = sessionExtractParams.additionalHeaders.toBuilder() + additionalQueryParams = sessionExtractParams.additionalQueryParams.toBuilder() + } + + /** Unique session identifier */ + fun id(id: String?) = apply { this.id = id } + + /** Alias for calling [Builder.id] with `id.orElse(null)`. */ + fun id(id: Optional) = id(id.getOrNull()) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { + this.xStreamResponse = xStreamResponse + } + + /** Alias for calling [Builder.xStreamResponse] with `xStreamResponse.orElse(null)`. */ + 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: + * - [frameId] + * - [instruction] + * - [options] + * - [schema] + */ + fun body(body: Body) = apply { this.body = body.toBuilder() } + + /** Target frame ID for the extraction */ + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun frameId(frameId: JsonField) = apply { body.frameId(frameId) } + + /** Natural language instruction for what to extract */ + fun instruction(instruction: String) = apply { body.instruction(instruction) } + + /** + * Sets [Builder.instruction] to an arbitrary JSON value. + * + * You should usually call [Builder.instruction] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun instruction(instruction: JsonField) = apply { body.instruction(instruction) } + + fun options(options: Options) = apply { body.options(options) } + + /** + * Sets [Builder.options] to an arbitrary JSON value. + * + * You should usually call [Builder.options] with a well-typed [Options] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun options(options: JsonField) = apply { body.options(options) } + + /** JSON Schema defining the structure of data to extract */ + fun schema(schema: Schema) = apply { body.schema(schema) } + + /** + * Sets [Builder.schema] to an arbitrary JSON value. + * + * You should usually call [Builder.schema] with a well-typed [Schema] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun schema(schema: JsonField) = apply { body.schema(schema) } + + 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) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + additionalQueryParams.removeAll(keys) + } + + /** + * Returns an immutable instance of [SessionExtractParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): SessionExtractParams = + SessionExtractParams( + id, + xStreamResponse, + body.build(), + additionalHeaders.build(), + additionalQueryParams.build(), + ) + } + + fun _body(): Body = body + + fun _pathParam(index: Int): String = + when (index) { + 0 -> id ?: "" + else -> "" + } + + override fun _headers(): Headers = + Headers.builder() + .apply { + xStreamResponse?.let { put("x-stream-response", it.toString()) } + putAll(additionalHeaders) + } + .build() + + override fun _queryParams(): QueryParams = additionalQueryParams + + class Body + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val frameId: JsonField, + private val instruction: JsonField, + private val options: JsonField, + private val schema: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("frameId") @ExcludeMissing frameId: JsonField = JsonMissing.of(), + @JsonProperty("instruction") + @ExcludeMissing + instruction: JsonField = JsonMissing.of(), + @JsonProperty("options") @ExcludeMissing options: JsonField = JsonMissing.of(), + @JsonProperty("schema") @ExcludeMissing schema: JsonField = JsonMissing.of(), + ) : this(frameId, instruction, options, schema, mutableMapOf()) + + /** + * Target frame ID for the extraction + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun frameId(): Optional = frameId.getOptional("frameId") + + /** + * Natural language instruction for what to extract + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun instruction(): Optional = instruction.getOptional("instruction") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun options(): Optional = options.getOptional("options") + + /** + * JSON Schema defining the structure of data to extract + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun schema(): Optional = schema.getOptional("schema") + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("frameId") @ExcludeMissing fun _frameId(): JsonField = frameId + + /** + * Returns the raw JSON value of [instruction]. + * + * Unlike [instruction], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("instruction") + @ExcludeMissing + fun _instruction(): JsonField = instruction + + /** + * Returns the raw JSON value of [options]. + * + * Unlike [options], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("options") @ExcludeMissing fun _options(): JsonField = options + + /** + * Returns the raw JSON value of [schema]. + * + * Unlike [schema], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("schema") @ExcludeMissing fun _schema(): JsonField = schema + + @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 frameId: JsonField = JsonMissing.of() + private var instruction: JsonField = JsonMissing.of() + private var options: JsonField = JsonMissing.of() + private var schema: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(body: Body) = apply { + frameId = body.frameId + instruction = body.instruction + options = body.options + schema = body.schema + additionalProperties = body.additionalProperties.toMutableMap() + } + + /** Target frame ID for the extraction */ + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun frameId(frameId: JsonField) = apply { this.frameId = frameId } + + /** Natural language instruction for what to extract */ + fun instruction(instruction: String) = instruction(JsonField.of(instruction)) + + /** + * Sets [Builder.instruction] to an arbitrary JSON value. + * + * You should usually call [Builder.instruction] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun instruction(instruction: JsonField) = apply { + this.instruction = instruction + } + + fun options(options: Options) = options(JsonField.of(options)) + + /** + * Sets [Builder.options] to an arbitrary JSON value. + * + * You should usually call [Builder.options] with a well-typed [Options] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun options(options: JsonField) = apply { this.options = options } + + /** JSON Schema defining the structure of data to extract */ + fun schema(schema: Schema) = schema(JsonField.of(schema)) + + /** + * Sets [Builder.schema] to an arbitrary JSON value. + * + * You should usually call [Builder.schema] with a well-typed [Schema] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun schema(schema: JsonField) = apply { this.schema = schema } + + 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(frameId, instruction, options, schema, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Body = apply { + if (validated) { + return@apply + } + + frameId() + instruction() + options().ifPresent { it.validate() } + schema().ifPresent { it.validate() } + 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 = + (if (frameId.asKnown().isPresent) 1 else 0) + + (if (instruction.asKnown().isPresent) 1 else 0) + + (options.asKnown().getOrNull()?.validity() ?: 0) + + (schema.asKnown().getOrNull()?.validity() ?: 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Body && + frameId == other.frameId && + instruction == other.instruction && + options == other.options && + schema == other.schema && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(frameId, instruction, options, schema, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Body{frameId=$frameId, instruction=$instruction, options=$options, schema=$schema, additionalProperties=$additionalProperties}" + } + + class Options + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val ignoreSelectors: JsonField>, + private val model: JsonField, + private val selector: JsonField, + private val timeout: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("ignoreSelectors") + @ExcludeMissing + ignoreSelectors: JsonField> = JsonMissing.of(), + @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), + @JsonProperty("selector") + @ExcludeMissing + selector: JsonField = JsonMissing.of(), + @JsonProperty("timeout") @ExcludeMissing timeout: JsonField = JsonMissing.of(), + ) : this(ignoreSelectors, model, selector, timeout, mutableMapOf()) + + /** + * Selectors for elements and subtrees that should be excluded from extraction + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun ignoreSelectors(): Optional> = + ignoreSelectors.getOptional("ignoreSelectors") + + /** + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun model(): Optional = model.getOptional("model") + + /** + * CSS selector to scope extraction to a specific element + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun selector(): Optional = selector.getOptional("selector") + + /** + * Timeout in ms for the extraction + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun timeout(): Optional = timeout.getOptional("timeout") + + /** + * Returns the raw JSON value of [ignoreSelectors]. + * + * Unlike [ignoreSelectors], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("ignoreSelectors") + @ExcludeMissing + fun _ignoreSelectors(): JsonField> = ignoreSelectors + + /** + * Returns the raw JSON value of [model]. + * + * Unlike [model], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model + + /** + * Returns the raw JSON value of [selector]. + * + * Unlike [selector], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("selector") @ExcludeMissing fun _selector(): JsonField = selector + + /** + * Returns the raw JSON value of [timeout]. + * + * Unlike [timeout], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("timeout") @ExcludeMissing fun _timeout(): JsonField = timeout + + @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 [Options]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Options]. */ + class Builder internal constructor() { + + private var ignoreSelectors: JsonField>? = null + private var model: JsonField = JsonMissing.of() + private var selector: JsonField = JsonMissing.of() + private var timeout: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(options: Options) = apply { + ignoreSelectors = options.ignoreSelectors.map { it.toMutableList() } + model = options.model + selector = options.selector + timeout = options.timeout + additionalProperties = options.additionalProperties.toMutableMap() + } + + /** Selectors for elements and subtrees that should be excluded from extraction */ + fun ignoreSelectors(ignoreSelectors: List) = + ignoreSelectors(JsonField.of(ignoreSelectors)) + + /** + * Sets [Builder.ignoreSelectors] to an arbitrary JSON value. + * + * You should usually call [Builder.ignoreSelectors] with a well-typed `List` + * value instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun ignoreSelectors(ignoreSelectors: JsonField>) = apply { + this.ignoreSelectors = ignoreSelectors.map { it.toMutableList() } + } + + /** + * Adds a single [String] to [ignoreSelectors]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addIgnoreSelector(ignoreSelector: String) = apply { + ignoreSelectors = + (ignoreSelectors ?: JsonField.of(mutableListOf())).also { + checkKnown("ignoreSelectors", it).add(ignoreSelector) + } + } + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + fun model(model: Model) = model(JsonField.of(model)) + + /** + * Sets [Builder.model] to an arbitrary JSON value. + * + * You should usually call [Builder.model] with a well-typed [Model] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun model(model: JsonField) = apply { this.model = model } + + /** Alias for calling [model] with `Model.ofConfig(config)`. */ + fun model(config: ModelConfig) = model(Model.ofConfig(config)) + + /** Alias for calling [model] with `Model.ofString(string)`. */ + fun model(string: String) = model(Model.ofString(string)) + + /** CSS selector to scope extraction to a specific element */ + fun selector(selector: String) = selector(JsonField.of(selector)) + + /** + * Sets [Builder.selector] to an arbitrary JSON value. + * + * You should usually call [Builder.selector] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun selector(selector: JsonField) = apply { this.selector = selector } + + /** Timeout in ms for the extraction */ + fun timeout(timeout: Double) = timeout(JsonField.of(timeout)) + + /** + * Sets [Builder.timeout] to an arbitrary JSON value. + * + * You should usually call [Builder.timeout] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun timeout(timeout: JsonField) = apply { this.timeout = timeout } + + 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 [Options]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Options = + Options( + (ignoreSelectors ?: JsonMissing.of()).map { it.toImmutable() }, + model, + selector, + timeout, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Options = apply { + if (validated) { + return@apply + } + + ignoreSelectors() + model().ifPresent { it.validate() } + selector() + timeout() + 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 = + (ignoreSelectors.asKnown().getOrNull()?.size ?: 0) + + (model.asKnown().getOrNull()?.validity() ?: 0) + + (if (selector.asKnown().isPresent) 1 else 0) + + (if (timeout.asKnown().isPresent) 1 else 0) + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + @JsonDeserialize(using = Model.Deserializer::class) + @JsonSerialize(using = Model.Serializer::class) + class Model + private constructor( + private val config: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun config(): Optional = Optional.ofNullable(config) + + fun string(): Optional = Optional.ofNullable(string) + + fun isConfig(): Boolean = config != null + + fun isString(): Boolean = string != null + + fun asConfig(): ModelConfig = config.getOrThrow("config") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given + * [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, + * unless [visitor] overrides [Visitor.unknown]. To handle variants not known to this + * version of the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = model.accept(new Model.Visitor>() { + * @Override + * public Optional visitConfig(ModelConfig config) { + * return Optional.of(config.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in + * [visitor] and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + config != null -> visitor.visitConfig(config) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Model = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) { + config.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) = config.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Model && config == other.config && string == other.string + } + + override fun hashCode(): Int = Objects.hash(config, string) + + override fun toString(): String = + when { + config != null -> "Model{config=$config}" + string != null -> "Model{string=$string}" + _json != null -> "Model{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Model") + } + + companion object { + + @JvmStatic fun ofConfig(config: ModelConfig) = Model(config = config) + + @JvmStatic fun ofString(string: String) = Model(string = string) + } + + /** + * An interface that defines how to map each variant of [Model] to a value of type [T]. + */ + interface Visitor { + + fun visitConfig(config: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [Model] to a value of type [T]. + * + * An instance of [Model] can contain an unknown variant if it was deserialized from + * data that doesn't match any known variant. For example, if the SDK is on an older + * version than the API, then the API may respond with new variants that the SDK is + * unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Model: $json") + } + } + + internal class Deserializer : BaseDeserializer(Model::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Model { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Model(config = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + Model(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> Model(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Model::class) { + + override fun serialize( + value: Model, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.config != null -> generator.writeObject(value.config) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Model") + } + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Options && + ignoreSelectors == other.ignoreSelectors && + model == other.model && + selector == other.selector && + timeout == other.timeout && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(ignoreSelectors, model, selector, timeout, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Options{ignoreSelectors=$ignoreSelectors, model=$model, selector=$selector, timeout=$timeout, additionalProperties=$additionalProperties}" + } + + /** JSON Schema defining the structure of data to extract */ + class Schema + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Schema]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Schema]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(schema: Schema) = apply { + additionalProperties = schema.additionalProperties.toMutableMap() + } + + 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 [Schema]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Schema = Schema(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Schema = 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 = + additionalProperties.count { (_, value) -> !value.isNull() && !value.isMissing() } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Schema && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Schema{additionalProperties=$additionalProperties}" + } + + /** Whether to stream the response via SSE */ + class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val TRUE = of("true") + + @JvmField val FALSE = of("false") + + @JvmStatic fun of(value: String) = XStreamResponse(JsonField.of(value)) + } + + /** An enum containing [XStreamResponse]'s known values. */ + enum class Known { + TRUE, + FALSE, + } + + /** + * An enum containing [XStreamResponse]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [XStreamResponse] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + TRUE, + FALSE, + /** + * An enum member indicating that [XStreamResponse] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + TRUE -> Value.TRUE + FALSE -> Value.FALSE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + TRUE -> Known.TRUE + FALSE -> Known.FALSE + else -> throw StagehandInvalidDataException("Unknown XStreamResponse: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): XStreamResponse = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is XStreamResponse && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionExtractParams && + id == other.id && + xStreamResponse == other.xStreamResponse && + body == other.body && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams + } + + override fun hashCode(): Int = + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) + + override fun toString() = + "SessionExtractParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractResponse.kt new file mode 100644 index 0000000..e029256 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractResponse.kt @@ -0,0 +1,400 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +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.checkRequired +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class SessionExtractResponse +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val data: JsonField, + private val success: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("success") @ExcludeMissing success: JsonField = JsonMissing.of(), + ) : this(data, success, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun data(): Data = data.getRequired("data") + + /** + * Indicates whether the request was successful + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [data]. + * + * Unlike [data], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("data") @ExcludeMissing fun _data(): JsonField = data + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @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 [SessionExtractResponse]. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionExtractResponse]. */ + class Builder internal constructor() { + + private var data: JsonField? = null + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(sessionExtractResponse: SessionExtractResponse) = apply { + data = sessionExtractResponse.data + success = sessionExtractResponse.success + additionalProperties = sessionExtractResponse.additionalProperties.toMutableMap() + } + + fun data(data: Data) = data(JsonField.of(data)) + + /** + * Sets [Builder.data] to an arbitrary JSON value. + * + * You should usually call [Builder.data] with a well-typed [Data] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun data(data: JsonField) = apply { this.data = data } + + /** Indicates whether the request was successful */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [SessionExtractResponse]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionExtractResponse = + SessionExtractResponse( + checkRequired("data", data), + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): SessionExtractResponse = apply { + if (validated) { + return@apply + } + + data().validate() + success() + 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 = + (data.asKnown().getOrNull()?.validity() ?: 0) + (if (success.asKnown().isPresent) 1 else 0) + + class Data + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val result: JsonValue, + private val actionId: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("result") @ExcludeMissing result: JsonValue = JsonMissing.of(), + @JsonProperty("actionId") @ExcludeMissing actionId: JsonField = JsonMissing.of(), + ) : this(result, actionId, mutableMapOf()) + + /** + * Extracted data matching the requested schema + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = data.result().convert(MyClass.class); + * ``` + */ + @JsonProperty("result") @ExcludeMissing fun _result(): JsonValue = result + + /** + * Action ID for tracking + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun actionId(): Optional = actionId.getOptional("actionId") + + /** + * Returns the raw JSON value of [actionId]. + * + * Unlike [actionId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("actionId") @ExcludeMissing fun _actionId(): JsonField = actionId + + @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 [Data]. + * + * The following fields are required: + * ```java + * .result() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Data]. */ + class Builder internal constructor() { + + private var result: JsonValue? = null + private var actionId: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(data: Data) = apply { + result = data.result + actionId = data.actionId + additionalProperties = data.additionalProperties.toMutableMap() + } + + /** Extracted data matching the requested schema */ + fun result(result: JsonValue) = apply { this.result = result } + + /** Action ID for tracking */ + fun actionId(actionId: String) = actionId(JsonField.of(actionId)) + + /** + * Sets [Builder.actionId] to an arbitrary JSON value. + * + * You should usually call [Builder.actionId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun actionId(actionId: JsonField) = apply { this.actionId = actionId } + + 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 [Data]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .result() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Data = + Data(checkRequired("result", result), actionId, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Data = apply { + if (validated) { + return@apply + } + + actionId() + 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 = (if (actionId.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Data && + result == other.result && + actionId == other.actionId && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(result, actionId, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Data{result=$result, actionId=$actionId, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionExtractResponse && + data == other.data && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(data, success, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "SessionExtractResponse{data=$data, success=$success, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateParams.kt new file mode 100644 index 0000000..7abd8a2 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateParams.kt @@ -0,0 +1,1191 @@ +// File generated from our OpenAPI spec by Stainless. + +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.checkRequired +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.QueryParams +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** Navigates the browser to the specified URL. */ +class SessionNavigateParams +private constructor( + private val id: String?, + private val xStreamResponse: XStreamResponse?, + private val body: Body, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, +) : Params { + + /** Unique session identifier */ + fun id(): Optional = Optional.ofNullable(id) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) + + /** + * URL to navigate to + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun url(): String = body.url() + + /** + * Target frame ID for the navigation + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun frameId(): Optional = body.frameId() + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun options(): Optional = body.options() + + /** + * Whether to stream the response via SSE + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun streamResponse(): Optional = body.streamResponse() + + /** + * Returns the raw JSON value of [url]. + * + * Unlike [url], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _url(): JsonField = body._url() + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _frameId(): JsonField = body._frameId() + + /** + * Returns the raw JSON value of [options]. + * + * Unlike [options], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _options(): JsonField = body._options() + + /** + * Returns the raw JSON value of [streamResponse]. + * + * Unlike [streamResponse], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _streamResponse(): JsonField = body._streamResponse() + + fun _additionalBodyProperties(): Map = body._additionalProperties() + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [SessionNavigateParams]. + * + * The following fields are required: + * ```java + * .url() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionNavigateParams]. */ + class Builder internal constructor() { + + private var id: String? = 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() + + @JvmSynthetic + internal fun from(sessionNavigateParams: SessionNavigateParams) = apply { + id = sessionNavigateParams.id + xStreamResponse = sessionNavigateParams.xStreamResponse + body = sessionNavigateParams.body.toBuilder() + additionalHeaders = sessionNavigateParams.additionalHeaders.toBuilder() + additionalQueryParams = sessionNavigateParams.additionalQueryParams.toBuilder() + } + + /** Unique session identifier */ + fun id(id: String?) = apply { this.id = id } + + /** Alias for calling [Builder.id] with `id.orElse(null)`. */ + fun id(id: Optional) = id(id.getOrNull()) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { + this.xStreamResponse = xStreamResponse + } + + /** Alias for calling [Builder.xStreamResponse] with `xStreamResponse.orElse(null)`. */ + 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: + * - [url] + * - [frameId] + * - [options] + * - [streamResponse] + */ + fun body(body: Body) = apply { this.body = body.toBuilder() } + + /** URL to navigate to */ + fun url(url: String) = apply { body.url(url) } + + /** + * Sets [Builder.url] to an arbitrary JSON value. + * + * You should usually call [Builder.url] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun url(url: JsonField) = apply { body.url(url) } + + /** Target frame ID for the navigation */ + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun frameId(frameId: JsonField) = apply { body.frameId(frameId) } + + fun options(options: Options) = apply { body.options(options) } + + /** + * Sets [Builder.options] to an arbitrary JSON value. + * + * You should usually call [Builder.options] with a well-typed [Options] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun options(options: JsonField) = apply { body.options(options) } + + /** Whether to stream the response via SSE */ + fun streamResponse(streamResponse: Boolean) = apply { body.streamResponse(streamResponse) } + + /** + * Sets [Builder.streamResponse] to an arbitrary JSON value. + * + * You should usually call [Builder.streamResponse] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun streamResponse(streamResponse: JsonField) = apply { + body.streamResponse(streamResponse) + } + + 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) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + additionalQueryParams.removeAll(keys) + } + + /** + * Returns an immutable instance of [SessionNavigateParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .url() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionNavigateParams = + SessionNavigateParams( + id, + xStreamResponse, + body.build(), + additionalHeaders.build(), + additionalQueryParams.build(), + ) + } + + fun _body(): Body = body + + fun _pathParam(index: Int): String = + when (index) { + 0 -> id ?: "" + else -> "" + } + + override fun _headers(): Headers = + Headers.builder() + .apply { + xStreamResponse?.let { put("x-stream-response", it.toString()) } + putAll(additionalHeaders) + } + .build() + + override fun _queryParams(): QueryParams = additionalQueryParams + + class Body + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val url: JsonField, + private val frameId: JsonField, + private val options: JsonField, + private val streamResponse: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("url") @ExcludeMissing url: JsonField = JsonMissing.of(), + @JsonProperty("frameId") @ExcludeMissing frameId: JsonField = JsonMissing.of(), + @JsonProperty("options") @ExcludeMissing options: JsonField = JsonMissing.of(), + @JsonProperty("streamResponse") + @ExcludeMissing + streamResponse: JsonField = JsonMissing.of(), + ) : this(url, frameId, options, streamResponse, mutableMapOf()) + + /** + * URL to navigate to + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun url(): String = url.getRequired("url") + + /** + * Target frame ID for the navigation + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun frameId(): Optional = frameId.getOptional("frameId") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun options(): Optional = options.getOptional("options") + + /** + * Whether to stream the response via SSE + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun streamResponse(): Optional = streamResponse.getOptional("streamResponse") + + /** + * Returns the raw JSON value of [url]. + * + * Unlike [url], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("url") @ExcludeMissing fun _url(): JsonField = url + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("frameId") @ExcludeMissing fun _frameId(): JsonField = frameId + + /** + * Returns the raw JSON value of [options]. + * + * Unlike [options], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("options") @ExcludeMissing fun _options(): JsonField = options + + /** + * Returns the raw JSON value of [streamResponse]. + * + * Unlike [streamResponse], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("streamResponse") + @ExcludeMissing + fun _streamResponse(): JsonField = streamResponse + + @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]. + * + * The following fields are required: + * ```java + * .url() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Body]. */ + class Builder internal constructor() { + + private var url: JsonField? = null + private var frameId: JsonField = JsonMissing.of() + private var options: JsonField = JsonMissing.of() + private var streamResponse: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(body: Body) = apply { + url = body.url + frameId = body.frameId + options = body.options + streamResponse = body.streamResponse + additionalProperties = body.additionalProperties.toMutableMap() + } + + /** URL to navigate to */ + fun url(url: String) = url(JsonField.of(url)) + + /** + * Sets [Builder.url] to an arbitrary JSON value. + * + * You should usually call [Builder.url] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun url(url: JsonField) = apply { this.url = url } + + /** Target frame ID for the navigation */ + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun frameId(frameId: JsonField) = apply { this.frameId = frameId } + + fun options(options: Options) = options(JsonField.of(options)) + + /** + * Sets [Builder.options] to an arbitrary JSON value. + * + * You should usually call [Builder.options] with a well-typed [Options] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun options(options: JsonField) = apply { this.options = options } + + /** Whether to stream the response via SSE */ + fun streamResponse(streamResponse: Boolean) = + streamResponse(JsonField.of(streamResponse)) + + /** + * Sets [Builder.streamResponse] to an arbitrary JSON value. + * + * You should usually call [Builder.streamResponse] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun streamResponse(streamResponse: JsonField) = apply { + this.streamResponse = streamResponse + } + + 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. + * + * The following fields are required: + * ```java + * .url() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Body = + Body( + checkRequired("url", url), + frameId, + options, + streamResponse, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Body = apply { + if (validated) { + return@apply + } + + url() + frameId() + options().ifPresent { it.validate() } + streamResponse() + 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 = + (if (url.asKnown().isPresent) 1 else 0) + + (if (frameId.asKnown().isPresent) 1 else 0) + + (options.asKnown().getOrNull()?.validity() ?: 0) + + (if (streamResponse.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Body && + url == other.url && + frameId == other.frameId && + options == other.options && + streamResponse == other.streamResponse && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(url, frameId, options, streamResponse, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Body{url=$url, frameId=$frameId, options=$options, streamResponse=$streamResponse, additionalProperties=$additionalProperties}" + } + + class Options + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val referer: JsonField, + private val timeout: JsonField, + private val waitUntil: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("referer") @ExcludeMissing referer: JsonField = JsonMissing.of(), + @JsonProperty("timeout") @ExcludeMissing timeout: JsonField = JsonMissing.of(), + @JsonProperty("waitUntil") + @ExcludeMissing + waitUntil: JsonField = JsonMissing.of(), + ) : this(referer, timeout, waitUntil, mutableMapOf()) + + /** + * Referer header to send with the request + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun referer(): Optional = referer.getOptional("referer") + + /** + * Timeout in ms for the navigation + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun timeout(): Optional = timeout.getOptional("timeout") + + /** + * When to consider navigation complete + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun waitUntil(): Optional = waitUntil.getOptional("waitUntil") + + /** + * Returns the raw JSON value of [referer]. + * + * Unlike [referer], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("referer") @ExcludeMissing fun _referer(): JsonField = referer + + /** + * Returns the raw JSON value of [timeout]. + * + * Unlike [timeout], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("timeout") @ExcludeMissing fun _timeout(): JsonField = timeout + + /** + * Returns the raw JSON value of [waitUntil]. + * + * Unlike [waitUntil], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("waitUntil") + @ExcludeMissing + fun _waitUntil(): JsonField = waitUntil + + @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 [Options]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Options]. */ + class Builder internal constructor() { + + private var referer: JsonField = JsonMissing.of() + private var timeout: JsonField = JsonMissing.of() + private var waitUntil: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(options: Options) = apply { + referer = options.referer + timeout = options.timeout + waitUntil = options.waitUntil + additionalProperties = options.additionalProperties.toMutableMap() + } + + /** Referer header to send with the request */ + fun referer(referer: String) = referer(JsonField.of(referer)) + + /** + * Sets [Builder.referer] to an arbitrary JSON value. + * + * You should usually call [Builder.referer] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun referer(referer: JsonField) = apply { this.referer = referer } + + /** Timeout in ms for the navigation */ + fun timeout(timeout: Double) = timeout(JsonField.of(timeout)) + + /** + * Sets [Builder.timeout] to an arbitrary JSON value. + * + * You should usually call [Builder.timeout] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun timeout(timeout: JsonField) = apply { this.timeout = timeout } + + /** When to consider navigation complete */ + fun waitUntil(waitUntil: WaitUntil) = waitUntil(JsonField.of(waitUntil)) + + /** + * Sets [Builder.waitUntil] to an arbitrary JSON value. + * + * You should usually call [Builder.waitUntil] with a well-typed [WaitUntil] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun waitUntil(waitUntil: JsonField) = apply { this.waitUntil = waitUntil } + + 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 [Options]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Options = + Options(referer, timeout, waitUntil, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Options = apply { + if (validated) { + return@apply + } + + referer() + timeout() + waitUntil().ifPresent { it.validate() } + 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 = + (if (referer.asKnown().isPresent) 1 else 0) + + (if (timeout.asKnown().isPresent) 1 else 0) + + (waitUntil.asKnown().getOrNull()?.validity() ?: 0) + + /** When to consider navigation complete */ + class WaitUntil @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is + * on an older version than the API, then the API may respond with new members that the + * SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val LOAD = of("load") + + @JvmField val DOMCONTENTLOADED = of("domcontentloaded") + + @JvmField val NETWORKIDLE = of("networkidle") + + @JvmStatic fun of(value: String) = WaitUntil(JsonField.of(value)) + } + + /** An enum containing [WaitUntil]'s known values. */ + enum class Known { + LOAD, + DOMCONTENTLOADED, + NETWORKIDLE, + } + + /** + * An enum containing [WaitUntil]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [WaitUntil] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + LOAD, + DOMCONTENTLOADED, + NETWORKIDLE, + /** + * An enum member indicating that [WaitUntil] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you + * want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + LOAD -> Value.LOAD + DOMCONTENTLOADED -> Value.DOMCONTENTLOADED + NETWORKIDLE -> Value.NETWORKIDLE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + LOAD -> Known.LOAD + DOMCONTENTLOADED -> Known.DOMCONTENTLOADED + NETWORKIDLE -> Known.NETWORKIDLE + else -> throw StagehandInvalidDataException("Unknown WaitUntil: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have + * the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): WaitUntil = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is WaitUntil && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Options && + referer == other.referer && + timeout == other.timeout && + waitUntil == other.waitUntil && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(referer, timeout, waitUntil, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Options{referer=$referer, timeout=$timeout, waitUntil=$waitUntil, additionalProperties=$additionalProperties}" + } + + /** Whether to stream the response via SSE */ + class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val TRUE = of("true") + + @JvmField val FALSE = of("false") + + @JvmStatic fun of(value: String) = XStreamResponse(JsonField.of(value)) + } + + /** An enum containing [XStreamResponse]'s known values. */ + enum class Known { + TRUE, + FALSE, + } + + /** + * An enum containing [XStreamResponse]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [XStreamResponse] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + TRUE, + FALSE, + /** + * An enum member indicating that [XStreamResponse] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + TRUE -> Value.TRUE + FALSE -> Value.FALSE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + TRUE -> Known.TRUE + FALSE -> Known.FALSE + else -> throw StagehandInvalidDataException("Unknown XStreamResponse: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): XStreamResponse = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is XStreamResponse && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionNavigateParams && + id == other.id && + xStreamResponse == other.xStreamResponse && + body == other.body && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams + } + + override fun hashCode(): Int = + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) + + override fun toString() = + "SessionNavigateParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponse.kt new file mode 100644 index 0000000..a4ccaa6 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponse.kt @@ -0,0 +1,400 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +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.checkRequired +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class SessionNavigateResponse +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val data: JsonField, + private val success: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("success") @ExcludeMissing success: JsonField = JsonMissing.of(), + ) : this(data, success, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun data(): Data = data.getRequired("data") + + /** + * Indicates whether the request was successful + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [data]. + * + * Unlike [data], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("data") @ExcludeMissing fun _data(): JsonField = data + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @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 [SessionNavigateResponse]. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionNavigateResponse]. */ + class Builder internal constructor() { + + private var data: JsonField? = null + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(sessionNavigateResponse: SessionNavigateResponse) = apply { + data = sessionNavigateResponse.data + success = sessionNavigateResponse.success + additionalProperties = sessionNavigateResponse.additionalProperties.toMutableMap() + } + + fun data(data: Data) = data(JsonField.of(data)) + + /** + * Sets [Builder.data] to an arbitrary JSON value. + * + * You should usually call [Builder.data] with a well-typed [Data] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun data(data: JsonField) = apply { this.data = data } + + /** Indicates whether the request was successful */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [SessionNavigateResponse]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionNavigateResponse = + SessionNavigateResponse( + checkRequired("data", data), + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): SessionNavigateResponse = apply { + if (validated) { + return@apply + } + + data().validate() + success() + 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 = + (data.asKnown().getOrNull()?.validity() ?: 0) + (if (success.asKnown().isPresent) 1 else 0) + + class Data + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val result: JsonValue, + private val actionId: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("result") @ExcludeMissing result: JsonValue = JsonMissing.of(), + @JsonProperty("actionId") @ExcludeMissing actionId: JsonField = JsonMissing.of(), + ) : this(result, actionId, mutableMapOf()) + + /** + * Navigation response (Playwright Response object or null) + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = data.result().convert(MyClass.class); + * ``` + */ + @JsonProperty("result") @ExcludeMissing fun _result(): JsonValue = result + + /** + * Action ID for tracking + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun actionId(): Optional = actionId.getOptional("actionId") + + /** + * Returns the raw JSON value of [actionId]. + * + * Unlike [actionId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("actionId") @ExcludeMissing fun _actionId(): JsonField = actionId + + @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 [Data]. + * + * The following fields are required: + * ```java + * .result() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Data]. */ + class Builder internal constructor() { + + private var result: JsonValue? = null + private var actionId: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(data: Data) = apply { + result = data.result + actionId = data.actionId + additionalProperties = data.additionalProperties.toMutableMap() + } + + /** Navigation response (Playwright Response object or null) */ + fun result(result: JsonValue) = apply { this.result = result } + + /** Action ID for tracking */ + fun actionId(actionId: String) = actionId(JsonField.of(actionId)) + + /** + * Sets [Builder.actionId] to an arbitrary JSON value. + * + * You should usually call [Builder.actionId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun actionId(actionId: JsonField) = apply { this.actionId = actionId } + + 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 [Data]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .result() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Data = + Data(checkRequired("result", result), actionId, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Data = apply { + if (validated) { + return@apply + } + + actionId() + 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 = (if (actionId.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Data && + result == other.result && + actionId == other.actionId && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(result, actionId, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Data{result=$result, actionId=$actionId, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionNavigateResponse && + data == other.data && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(data, success, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "SessionNavigateResponse{data=$data, success=$success, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveParams.kt new file mode 100644 index 0000000..dd8ffaa --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveParams.kt @@ -0,0 +1,1399 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.BaseDeserializer +import com.browserbase.api.core.BaseSerializer +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.allMaxBy +import com.browserbase.api.core.checkKnown +import com.browserbase.api.core.getOrThrow +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 com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import java.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** + * Identifies and returns available actions on the current page that match the given instruction. + */ +class SessionObserveParams +private constructor( + private val id: String?, + private val xStreamResponse: XStreamResponse?, + private val body: Body, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, +) : Params { + + /** Unique session identifier */ + fun id(): Optional = Optional.ofNullable(id) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) + + /** + * Target frame ID for the observation + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun frameId(): Optional = body.frameId() + + /** + * Natural language instruction for what actions to find + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun instruction(): Optional = body.instruction() + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun options(): Optional = body.options() + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _frameId(): JsonField = body._frameId() + + /** + * Returns the raw JSON value of [instruction]. + * + * Unlike [instruction], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _instruction(): JsonField = body._instruction() + + /** + * Returns the raw JSON value of [options]. + * + * Unlike [options], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _options(): JsonField = body._options() + + fun _additionalBodyProperties(): Map = body._additionalProperties() + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + @JvmStatic fun none(): SessionObserveParams = builder().build() + + /** Returns a mutable builder for constructing an instance of [SessionObserveParams]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionObserveParams]. */ + class Builder internal constructor() { + + private var id: String? = 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() + + @JvmSynthetic + internal fun from(sessionObserveParams: SessionObserveParams) = apply { + id = sessionObserveParams.id + xStreamResponse = sessionObserveParams.xStreamResponse + body = sessionObserveParams.body.toBuilder() + additionalHeaders = sessionObserveParams.additionalHeaders.toBuilder() + additionalQueryParams = sessionObserveParams.additionalQueryParams.toBuilder() + } + + /** Unique session identifier */ + fun id(id: String?) = apply { this.id = id } + + /** Alias for calling [Builder.id] with `id.orElse(null)`. */ + fun id(id: Optional) = id(id.getOrNull()) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { + this.xStreamResponse = xStreamResponse + } + + /** Alias for calling [Builder.xStreamResponse] with `xStreamResponse.orElse(null)`. */ + 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: + * - [frameId] + * - [instruction] + * - [options] + */ + fun body(body: Body) = apply { this.body = body.toBuilder() } + + /** Target frame ID for the observation */ + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun frameId(frameId: JsonField) = apply { body.frameId(frameId) } + + /** Natural language instruction for what actions to find */ + fun instruction(instruction: String) = apply { body.instruction(instruction) } + + /** + * Sets [Builder.instruction] to an arbitrary JSON value. + * + * You should usually call [Builder.instruction] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun instruction(instruction: JsonField) = apply { body.instruction(instruction) } + + fun options(options: Options) = apply { body.options(options) } + + /** + * Sets [Builder.options] to an arbitrary JSON value. + * + * You should usually call [Builder.options] with a well-typed [Options] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun options(options: JsonField) = apply { body.options(options) } + + 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) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + additionalQueryParams.removeAll(keys) + } + + /** + * Returns an immutable instance of [SessionObserveParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): SessionObserveParams = + SessionObserveParams( + id, + xStreamResponse, + body.build(), + additionalHeaders.build(), + additionalQueryParams.build(), + ) + } + + fun _body(): Body = body + + fun _pathParam(index: Int): String = + when (index) { + 0 -> id ?: "" + else -> "" + } + + override fun _headers(): Headers = + Headers.builder() + .apply { + xStreamResponse?.let { put("x-stream-response", it.toString()) } + putAll(additionalHeaders) + } + .build() + + override fun _queryParams(): QueryParams = additionalQueryParams + + class Body + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val frameId: JsonField, + private val instruction: JsonField, + private val options: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("frameId") @ExcludeMissing frameId: JsonField = JsonMissing.of(), + @JsonProperty("instruction") + @ExcludeMissing + instruction: JsonField = JsonMissing.of(), + @JsonProperty("options") @ExcludeMissing options: JsonField = JsonMissing.of(), + ) : this(frameId, instruction, options, mutableMapOf()) + + /** + * Target frame ID for the observation + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun frameId(): Optional = frameId.getOptional("frameId") + + /** + * Natural language instruction for what actions to find + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun instruction(): Optional = instruction.getOptional("instruction") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun options(): Optional = options.getOptional("options") + + /** + * Returns the raw JSON value of [frameId]. + * + * Unlike [frameId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("frameId") @ExcludeMissing fun _frameId(): JsonField = frameId + + /** + * Returns the raw JSON value of [instruction]. + * + * Unlike [instruction], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("instruction") + @ExcludeMissing + fun _instruction(): JsonField = instruction + + /** + * Returns the raw JSON value of [options]. + * + * Unlike [options], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("options") @ExcludeMissing fun _options(): JsonField = options + + @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 frameId: JsonField = JsonMissing.of() + private var instruction: JsonField = JsonMissing.of() + private var options: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(body: Body) = apply { + frameId = body.frameId + instruction = body.instruction + options = body.options + additionalProperties = body.additionalProperties.toMutableMap() + } + + /** Target frame ID for the observation */ + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) + + /** + * Sets [Builder.frameId] to an arbitrary JSON value. + * + * You should usually call [Builder.frameId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun frameId(frameId: JsonField) = apply { this.frameId = frameId } + + /** Natural language instruction for what actions to find */ + fun instruction(instruction: String) = instruction(JsonField.of(instruction)) + + /** + * Sets [Builder.instruction] to an arbitrary JSON value. + * + * You should usually call [Builder.instruction] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun instruction(instruction: JsonField) = apply { + this.instruction = instruction + } + + fun options(options: Options) = options(JsonField.of(options)) + + /** + * Sets [Builder.options] to an arbitrary JSON value. + * + * You should usually call [Builder.options] with a well-typed [Options] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun options(options: JsonField) = apply { this.options = options } + + 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(frameId, instruction, options, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Body = apply { + if (validated) { + return@apply + } + + frameId() + instruction() + options().ifPresent { it.validate() } + 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 = + (if (frameId.asKnown().isPresent) 1 else 0) + + (if (instruction.asKnown().isPresent) 1 else 0) + + (options.asKnown().getOrNull()?.validity() ?: 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Body && + frameId == other.frameId && + instruction == other.instruction && + options == other.options && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(frameId, instruction, options, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Body{frameId=$frameId, instruction=$instruction, options=$options, additionalProperties=$additionalProperties}" + } + + class Options + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val ignoreSelectors: JsonField>, + private val model: JsonField, + private val selector: JsonField, + private val timeout: JsonField, + private val variables: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("ignoreSelectors") + @ExcludeMissing + ignoreSelectors: JsonField> = JsonMissing.of(), + @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), + @JsonProperty("selector") + @ExcludeMissing + selector: JsonField = JsonMissing.of(), + @JsonProperty("timeout") @ExcludeMissing timeout: JsonField = JsonMissing.of(), + @JsonProperty("variables") + @ExcludeMissing + variables: JsonField = JsonMissing.of(), + ) : this(ignoreSelectors, model, selector, timeout, variables, mutableMapOf()) + + /** + * Selectors for elements and subtrees that should be excluded from observation + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun ignoreSelectors(): Optional> = + ignoreSelectors.getOptional("ignoreSelectors") + + /** + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun model(): Optional = model.getOptional("model") + + /** + * CSS selector to scope observation to a specific element + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun selector(): Optional = selector.getOptional("selector") + + /** + * Timeout in ms for the observation + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun timeout(): Optional = timeout.getOptional("timeout") + + /** + * Variables whose names are exposed to the model so observe() returns %variableName% + * placeholders in suggested action arguments instead of literal values. Accepts flat + * primitives or { value, description? } objects. + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun variables(): Optional = variables.getOptional("variables") + + /** + * Returns the raw JSON value of [ignoreSelectors]. + * + * Unlike [ignoreSelectors], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("ignoreSelectors") + @ExcludeMissing + fun _ignoreSelectors(): JsonField> = ignoreSelectors + + /** + * Returns the raw JSON value of [model]. + * + * Unlike [model], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model + + /** + * Returns the raw JSON value of [selector]. + * + * Unlike [selector], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("selector") @ExcludeMissing fun _selector(): JsonField = selector + + /** + * Returns the raw JSON value of [timeout]. + * + * Unlike [timeout], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("timeout") @ExcludeMissing fun _timeout(): JsonField = timeout + + /** + * Returns the raw JSON value of [variables]. + * + * Unlike [variables], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("variables") + @ExcludeMissing + fun _variables(): JsonField = variables + + @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 [Options]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Options]. */ + class Builder internal constructor() { + + private var ignoreSelectors: JsonField>? = null + private var model: JsonField = JsonMissing.of() + private var selector: JsonField = JsonMissing.of() + private var timeout: JsonField = JsonMissing.of() + private var variables: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(options: Options) = apply { + ignoreSelectors = options.ignoreSelectors.map { it.toMutableList() } + model = options.model + selector = options.selector + timeout = options.timeout + variables = options.variables + additionalProperties = options.additionalProperties.toMutableMap() + } + + /** Selectors for elements and subtrees that should be excluded from observation */ + fun ignoreSelectors(ignoreSelectors: List) = + ignoreSelectors(JsonField.of(ignoreSelectors)) + + /** + * Sets [Builder.ignoreSelectors] to an arbitrary JSON value. + * + * You should usually call [Builder.ignoreSelectors] with a well-typed `List` + * value instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun ignoreSelectors(ignoreSelectors: JsonField>) = apply { + this.ignoreSelectors = ignoreSelectors.map { it.toMutableList() } + } + + /** + * Adds a single [String] to [ignoreSelectors]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addIgnoreSelector(ignoreSelector: String) = apply { + ignoreSelectors = + (ignoreSelectors ?: JsonField.of(mutableListOf())).also { + checkKnown("ignoreSelectors", it).add(ignoreSelector) + } + } + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + fun model(model: Model) = model(JsonField.of(model)) + + /** + * Sets [Builder.model] to an arbitrary JSON value. + * + * You should usually call [Builder.model] with a well-typed [Model] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun model(model: JsonField) = apply { this.model = model } + + /** Alias for calling [model] with `Model.ofConfig(config)`. */ + fun model(config: ModelConfig) = model(Model.ofConfig(config)) + + /** Alias for calling [model] with `Model.ofString(string)`. */ + fun model(string: String) = model(Model.ofString(string)) + + /** CSS selector to scope observation to a specific element */ + fun selector(selector: String) = selector(JsonField.of(selector)) + + /** + * Sets [Builder.selector] to an arbitrary JSON value. + * + * You should usually call [Builder.selector] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun selector(selector: JsonField) = apply { this.selector = selector } + + /** Timeout in ms for the observation */ + fun timeout(timeout: Double) = timeout(JsonField.of(timeout)) + + /** + * Sets [Builder.timeout] to an arbitrary JSON value. + * + * You should usually call [Builder.timeout] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun timeout(timeout: JsonField) = apply { this.timeout = timeout } + + /** + * Variables whose names are exposed to the model so observe() returns %variableName% + * placeholders in suggested action arguments instead of literal values. Accepts flat + * primitives or { value, description? } objects. + */ + fun variables(variables: Variables) = variables(JsonField.of(variables)) + + /** + * Sets [Builder.variables] to an arbitrary JSON value. + * + * You should usually call [Builder.variables] with a well-typed [Variables] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun variables(variables: JsonField) = apply { this.variables = variables } + + 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 [Options]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Options = + Options( + (ignoreSelectors ?: JsonMissing.of()).map { it.toImmutable() }, + model, + selector, + timeout, + variables, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Options = apply { + if (validated) { + return@apply + } + + ignoreSelectors() + model().ifPresent { it.validate() } + selector() + timeout() + variables().ifPresent { it.validate() } + 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 = + (ignoreSelectors.asKnown().getOrNull()?.size ?: 0) + + (model.asKnown().getOrNull()?.validity() ?: 0) + + (if (selector.asKnown().isPresent) 1 else 0) + + (if (timeout.asKnown().isPresent) 1 else 0) + + (variables.asKnown().getOrNull()?.validity() ?: 0) + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + @JsonDeserialize(using = Model.Deserializer::class) + @JsonSerialize(using = Model.Serializer::class) + class Model + private constructor( + private val config: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun config(): Optional = Optional.ofNullable(config) + + fun string(): Optional = Optional.ofNullable(string) + + fun isConfig(): Boolean = config != null + + fun isString(): Boolean = string != null + + fun asConfig(): ModelConfig = config.getOrThrow("config") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given + * [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, + * unless [visitor] overrides [Visitor.unknown]. To handle variants not known to this + * version of the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = model.accept(new Model.Visitor>() { + * @Override + * public Optional visitConfig(ModelConfig config) { + * return Optional.of(config.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in + * [visitor] and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + config != null -> visitor.visitConfig(config) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Model = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) { + config.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) = config.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Model && config == other.config && string == other.string + } + + override fun hashCode(): Int = Objects.hash(config, string) + + override fun toString(): String = + when { + config != null -> "Model{config=$config}" + string != null -> "Model{string=$string}" + _json != null -> "Model{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Model") + } + + companion object { + + @JvmStatic fun ofConfig(config: ModelConfig) = Model(config = config) + + @JvmStatic fun ofString(string: String) = Model(string = string) + } + + /** + * An interface that defines how to map each variant of [Model] to a value of type [T]. + */ + interface Visitor { + + fun visitConfig(config: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [Model] to a value of type [T]. + * + * An instance of [Model] can contain an unknown variant if it was deserialized from + * data that doesn't match any known variant. For example, if the SDK is on an older + * version than the API, then the API may respond with new variants that the SDK is + * unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Model: $json") + } + } + + internal class Deserializer : BaseDeserializer(Model::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Model { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Model(config = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + Model(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> Model(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Model::class) { + + override fun serialize( + value: Model, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.config != null -> generator.writeObject(value.config) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Model") + } + } + } + } + + /** + * Variables whose names are exposed to the model so observe() returns %variableName% + * placeholders in suggested action arguments instead of literal values. Accepts flat + * primitives or { value, description? } objects. + */ + class Variables + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Variables]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Variables]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(variables: Variables) = apply { + additionalProperties = variables.additionalProperties.toMutableMap() + } + + 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 [Variables]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Variables = Variables(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Variables = 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 = + additionalProperties.count { (_, value) -> !value.isNull() && !value.isMissing() } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Variables && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Variables{additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Options && + ignoreSelectors == other.ignoreSelectors && + model == other.model && + selector == other.selector && + timeout == other.timeout && + variables == other.variables && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(ignoreSelectors, model, selector, timeout, variables, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Options{ignoreSelectors=$ignoreSelectors, model=$model, selector=$selector, timeout=$timeout, variables=$variables, additionalProperties=$additionalProperties}" + } + + /** Whether to stream the response via SSE */ + class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val TRUE = of("true") + + @JvmField val FALSE = of("false") + + @JvmStatic fun of(value: String) = XStreamResponse(JsonField.of(value)) + } + + /** An enum containing [XStreamResponse]'s known values. */ + enum class Known { + TRUE, + FALSE, + } + + /** + * An enum containing [XStreamResponse]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [XStreamResponse] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + TRUE, + FALSE, + /** + * An enum member indicating that [XStreamResponse] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + TRUE -> Value.TRUE + FALSE -> Value.FALSE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + TRUE -> Known.TRUE + FALSE -> Known.FALSE + else -> throw StagehandInvalidDataException("Unknown XStreamResponse: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): XStreamResponse = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is XStreamResponse && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionObserveParams && + id == other.id && + xStreamResponse == other.xStreamResponse && + body == other.body && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams + } + + override fun hashCode(): Int = + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) + + override fun toString() = + "SessionObserveParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveResponse.kt new file mode 100644 index 0000000..c28a2b3 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveResponse.kt @@ -0,0 +1,802 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +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.checkKnown +import com.browserbase.api.core.checkRequired +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class SessionObserveResponse +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val data: JsonField, + private val success: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("success") @ExcludeMissing success: JsonField = JsonMissing.of(), + ) : this(data, success, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun data(): Data = data.getRequired("data") + + /** + * Indicates whether the request was successful + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [data]. + * + * Unlike [data], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("data") @ExcludeMissing fun _data(): JsonField = data + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @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 [SessionObserveResponse]. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionObserveResponse]. */ + class Builder internal constructor() { + + private var data: JsonField? = null + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(sessionObserveResponse: SessionObserveResponse) = apply { + data = sessionObserveResponse.data + success = sessionObserveResponse.success + additionalProperties = sessionObserveResponse.additionalProperties.toMutableMap() + } + + fun data(data: Data) = data(JsonField.of(data)) + + /** + * Sets [Builder.data] to an arbitrary JSON value. + * + * You should usually call [Builder.data] with a well-typed [Data] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun data(data: JsonField) = apply { this.data = data } + + /** Indicates whether the request was successful */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [SessionObserveResponse]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionObserveResponse = + SessionObserveResponse( + checkRequired("data", data), + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): SessionObserveResponse = apply { + if (validated) { + return@apply + } + + data().validate() + success() + 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 = + (data.asKnown().getOrNull()?.validity() ?: 0) + (if (success.asKnown().isPresent) 1 else 0) + + class Data + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val result: JsonField>, + private val actionId: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("result") + @ExcludeMissing + result: JsonField> = JsonMissing.of(), + @JsonProperty("actionId") @ExcludeMissing actionId: JsonField = JsonMissing.of(), + ) : this(result, actionId, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun result(): List = result.getRequired("result") + + /** + * Action ID for tracking + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun actionId(): Optional = actionId.getOptional("actionId") + + /** + * Returns the raw JSON value of [result]. + * + * Unlike [result], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("result") @ExcludeMissing fun _result(): JsonField> = result + + /** + * Returns the raw JSON value of [actionId]. + * + * Unlike [actionId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("actionId") @ExcludeMissing fun _actionId(): JsonField = actionId + + @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 [Data]. + * + * The following fields are required: + * ```java + * .result() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Data]. */ + class Builder internal constructor() { + + private var result: JsonField>? = null + private var actionId: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(data: Data) = apply { + result = data.result.map { it.toMutableList() } + actionId = data.actionId + additionalProperties = data.additionalProperties.toMutableMap() + } + + fun result(result: List) = result(JsonField.of(result)) + + /** + * Sets [Builder.result] to an arbitrary JSON value. + * + * You should usually call [Builder.result] with a well-typed `List` value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun result(result: JsonField>) = apply { + this.result = result.map { it.toMutableList() } + } + + /** + * Adds a single [Result] to [Builder.result]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addResult(result: Result) = apply { + this.result = + (this.result ?: JsonField.of(mutableListOf())).also { + checkKnown("result", it).add(result) + } + } + + /** Action ID for tracking */ + fun actionId(actionId: String) = actionId(JsonField.of(actionId)) + + /** + * Sets [Builder.actionId] to an arbitrary JSON value. + * + * You should usually call [Builder.actionId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun actionId(actionId: JsonField) = apply { this.actionId = actionId } + + 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 [Data]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .result() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Data = + Data( + checkRequired("result", result).map { it.toImmutable() }, + actionId, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Data = apply { + if (validated) { + return@apply + } + + result().forEach { it.validate() } + actionId() + 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 = + (result.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + (if (actionId.asKnown().isPresent) 1 else 0) + + /** Action object returned by observe and used by act */ + class Result + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val description: JsonField, + private val selector: JsonField, + private val arguments: JsonField>, + private val backendNodeId: JsonField, + private val method: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("description") + @ExcludeMissing + description: JsonField = JsonMissing.of(), + @JsonProperty("selector") + @ExcludeMissing + selector: JsonField = JsonMissing.of(), + @JsonProperty("arguments") + @ExcludeMissing + arguments: JsonField> = JsonMissing.of(), + @JsonProperty("backendNodeId") + @ExcludeMissing + backendNodeId: JsonField = JsonMissing.of(), + @JsonProperty("method") @ExcludeMissing method: JsonField = JsonMissing.of(), + ) : this(description, selector, arguments, backendNodeId, method, mutableMapOf()) + + /** + * Human-readable description of the action + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun description(): String = description.getRequired("description") + + /** + * CSS selector or XPath for the element + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun selector(): String = selector.getRequired("selector") + + /** + * Arguments to pass to the method + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun arguments(): Optional> = arguments.getOptional("arguments") + + /** + * Backend node ID for the element + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun backendNodeId(): Optional = backendNodeId.getOptional("backendNodeId") + + /** + * The method to execute (click, fill, etc.) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun method(): Optional = method.getOptional("method") + + /** + * Returns the raw JSON value of [description]. + * + * Unlike [description], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("description") + @ExcludeMissing + fun _description(): JsonField = description + + /** + * Returns the raw JSON value of [selector]. + * + * Unlike [selector], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("selector") @ExcludeMissing fun _selector(): JsonField = selector + + /** + * Returns the raw JSON value of [arguments]. + * + * Unlike [arguments], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("arguments") + @ExcludeMissing + fun _arguments(): JsonField> = arguments + + /** + * Returns the raw JSON value of [backendNodeId]. + * + * Unlike [backendNodeId], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("backendNodeId") + @ExcludeMissing + fun _backendNodeId(): JsonField = backendNodeId + + /** + * Returns the raw JSON value of [method]. + * + * Unlike [method], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("method") @ExcludeMissing fun _method(): JsonField = method + + @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 [Result]. + * + * The following fields are required: + * ```java + * .description() + * .selector() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Result]. */ + class Builder internal constructor() { + + private var description: JsonField? = null + private var selector: JsonField? = null + private var arguments: JsonField>? = null + private var backendNodeId: JsonField = JsonMissing.of() + private var method: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(result: Result) = apply { + description = result.description + selector = result.selector + arguments = result.arguments.map { it.toMutableList() } + backendNodeId = result.backendNodeId + method = result.method + additionalProperties = result.additionalProperties.toMutableMap() + } + + /** Human-readable description of the action */ + fun description(description: String) = description(JsonField.of(description)) + + /** + * Sets [Builder.description] to an arbitrary JSON value. + * + * You should usually call [Builder.description] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun description(description: JsonField) = apply { + this.description = description + } + + /** CSS selector or XPath for the element */ + fun selector(selector: String) = selector(JsonField.of(selector)) + + /** + * Sets [Builder.selector] to an arbitrary JSON value. + * + * You should usually call [Builder.selector] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun selector(selector: JsonField) = apply { this.selector = selector } + + /** Arguments to pass to the method */ + fun arguments(arguments: List) = arguments(JsonField.of(arguments)) + + /** + * Sets [Builder.arguments] to an arbitrary JSON value. + * + * You should usually call [Builder.arguments] with a well-typed `List` + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun arguments(arguments: JsonField>) = apply { + this.arguments = arguments.map { it.toMutableList() } + } + + /** + * Adds a single [String] to [arguments]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addArgument(argument: String) = apply { + arguments = + (arguments ?: JsonField.of(mutableListOf())).also { + checkKnown("arguments", it).add(argument) + } + } + + /** Backend node ID for the element */ + fun backendNodeId(backendNodeId: Double) = + backendNodeId(JsonField.of(backendNodeId)) + + /** + * Sets [Builder.backendNodeId] to an arbitrary JSON value. + * + * You should usually call [Builder.backendNodeId] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun backendNodeId(backendNodeId: JsonField) = apply { + this.backendNodeId = backendNodeId + } + + /** The method to execute (click, fill, etc.) */ + fun method(method: String) = method(JsonField.of(method)) + + /** + * Sets [Builder.method] to an arbitrary JSON value. + * + * You should usually call [Builder.method] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun method(method: JsonField) = apply { this.method = method } + + 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 [Result]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .description() + * .selector() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Result = + Result( + checkRequired("description", description), + checkRequired("selector", selector), + (arguments ?: JsonMissing.of()).map { it.toImmutable() }, + backendNodeId, + method, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Result = apply { + if (validated) { + return@apply + } + + description() + selector() + arguments() + backendNodeId() + method() + 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 = + (if (description.asKnown().isPresent) 1 else 0) + + (if (selector.asKnown().isPresent) 1 else 0) + + (arguments.asKnown().getOrNull()?.size ?: 0) + + (if (backendNodeId.asKnown().isPresent) 1 else 0) + + (if (method.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Result && + description == other.description && + selector == other.selector && + arguments == other.arguments && + backendNodeId == other.backendNodeId && + method == other.method && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + description, + selector, + arguments, + backendNodeId, + method, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Result{description=$description, selector=$selector, arguments=$arguments, backendNodeId=$backendNodeId, method=$method, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Data && + result == other.result && + actionId == other.actionId && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(result, actionId, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Data{result=$result, actionId=$actionId, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionObserveResponse && + data == other.data && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(data, success, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "SessionObserveResponse{data=$data, success=$success, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayParams.kt new file mode 100644 index 0000000..2d25194 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayParams.kt @@ -0,0 +1,364 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.Enum +import com.browserbase.api.core.JsonField +import com.browserbase.api.core.Params +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.QueryParams +import com.browserbase.api.errors.StagehandInvalidDataException +import com.fasterxml.jackson.annotation.JsonCreator +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** Retrieves replay metrics for a session. */ +class SessionReplayParams +private constructor( + private val id: String?, + private val xStreamResponse: XStreamResponse?, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, +) : Params { + + /** Unique session identifier */ + fun id(): Optional = Optional.ofNullable(id) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + @JvmStatic fun none(): SessionReplayParams = builder().build() + + /** Returns a mutable builder for constructing an instance of [SessionReplayParams]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionReplayParams]. */ + class Builder internal constructor() { + + private var id: String? = null + private var xStreamResponse: XStreamResponse? = null + private var additionalHeaders: Headers.Builder = Headers.builder() + private var additionalQueryParams: QueryParams.Builder = QueryParams.builder() + + @JvmSynthetic + internal fun from(sessionReplayParams: SessionReplayParams) = apply { + id = sessionReplayParams.id + xStreamResponse = sessionReplayParams.xStreamResponse + additionalHeaders = sessionReplayParams.additionalHeaders.toBuilder() + additionalQueryParams = sessionReplayParams.additionalQueryParams.toBuilder() + } + + /** Unique session identifier */ + fun id(id: String?) = apply { this.id = id } + + /** Alias for calling [Builder.id] with `id.orElse(null)`. */ + fun id(id: Optional) = id(id.getOrNull()) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { + this.xStreamResponse = xStreamResponse + } + + /** Alias for calling [Builder.xStreamResponse] with `xStreamResponse.orElse(null)`. */ + fun xStreamResponse(xStreamResponse: Optional) = + xStreamResponse(xStreamResponse.getOrNull()) + + fun additionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + additionalQueryParams.removeAll(keys) + } + + /** + * Returns an immutable instance of [SessionReplayParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): SessionReplayParams = + SessionReplayParams( + id, + xStreamResponse, + additionalHeaders.build(), + additionalQueryParams.build(), + ) + } + + fun _pathParam(index: Int): String = + when (index) { + 0 -> id ?: "" + else -> "" + } + + override fun _headers(): Headers = + Headers.builder() + .apply { + xStreamResponse?.let { put("x-stream-response", it.toString()) } + putAll(additionalHeaders) + } + .build() + + override fun _queryParams(): QueryParams = additionalQueryParams + + /** Whether to stream the response via SSE */ + class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val TRUE = of("true") + + @JvmField val FALSE = of("false") + + @JvmStatic fun of(value: String) = XStreamResponse(JsonField.of(value)) + } + + /** An enum containing [XStreamResponse]'s known values. */ + enum class Known { + TRUE, + FALSE, + } + + /** + * An enum containing [XStreamResponse]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [XStreamResponse] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + TRUE, + FALSE, + /** + * An enum member indicating that [XStreamResponse] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + TRUE -> Value.TRUE + FALSE -> Value.FALSE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + TRUE -> Known.TRUE + FALSE -> Known.FALSE + else -> throw StagehandInvalidDataException("Unknown XStreamResponse: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): XStreamResponse = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is XStreamResponse && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionReplayParams && + id == other.id && + xStreamResponse == other.xStreamResponse && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams + } + + override fun hashCode(): Int = + Objects.hash(id, xStreamResponse, additionalHeaders, additionalQueryParams) + + override fun toString() = + "SessionReplayParams{id=$id, xStreamResponse=$xStreamResponse, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayResponse.kt new file mode 100644 index 0000000..a063510 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayResponse.kt @@ -0,0 +1,1644 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +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.checkKnown +import com.browserbase.api.core.checkRequired +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class SessionReplayResponse +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val data: JsonField, + private val success: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("success") @ExcludeMissing success: JsonField = JsonMissing.of(), + ) : this(data, success, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun data(): Data = data.getRequired("data") + + /** + * Indicates whether the request was successful + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [data]. + * + * Unlike [data], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("data") @ExcludeMissing fun _data(): JsonField = data + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @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 [SessionReplayResponse]. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionReplayResponse]. */ + class Builder internal constructor() { + + private var data: JsonField? = null + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(sessionReplayResponse: SessionReplayResponse) = apply { + data = sessionReplayResponse.data + success = sessionReplayResponse.success + additionalProperties = sessionReplayResponse.additionalProperties.toMutableMap() + } + + fun data(data: Data) = data(JsonField.of(data)) + + /** + * Sets [Builder.data] to an arbitrary JSON value. + * + * You should usually call [Builder.data] with a well-typed [Data] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun data(data: JsonField) = apply { this.data = data } + + /** Indicates whether the request was successful */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [SessionReplayResponse]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionReplayResponse = + SessionReplayResponse( + checkRequired("data", data), + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): SessionReplayResponse = apply { + if (validated) { + return@apply + } + + data().validate() + success() + 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 = + (data.asKnown().getOrNull()?.validity() ?: 0) + (if (success.asKnown().isPresent) 1 else 0) + + class Data + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val pages: JsonField>, + private val clientLanguage: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("pages") @ExcludeMissing pages: JsonField> = JsonMissing.of(), + @JsonProperty("clientLanguage") + @ExcludeMissing + clientLanguage: JsonField = JsonMissing.of(), + ) : this(pages, clientLanguage, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun pages(): List = pages.getRequired("pages") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun clientLanguage(): Optional = clientLanguage.getOptional("clientLanguage") + + /** + * Returns the raw JSON value of [pages]. + * + * Unlike [pages], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("pages") @ExcludeMissing fun _pages(): JsonField> = pages + + /** + * Returns the raw JSON value of [clientLanguage]. + * + * Unlike [clientLanguage], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("clientLanguage") + @ExcludeMissing + fun _clientLanguage(): JsonField = clientLanguage + + @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 [Data]. + * + * The following fields are required: + * ```java + * .pages() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Data]. */ + class Builder internal constructor() { + + private var pages: JsonField>? = null + private var clientLanguage: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(data: Data) = apply { + pages = data.pages.map { it.toMutableList() } + clientLanguage = data.clientLanguage + additionalProperties = data.additionalProperties.toMutableMap() + } + + fun pages(pages: List) = pages(JsonField.of(pages)) + + /** + * Sets [Builder.pages] to an arbitrary JSON value. + * + * You should usually call [Builder.pages] with a well-typed `List` value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun pages(pages: JsonField>) = apply { + this.pages = pages.map { it.toMutableList() } + } + + /** + * Adds a single [Page] to [pages]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addPage(page: Page) = apply { + pages = + (pages ?: JsonField.of(mutableListOf())).also { + checkKnown("pages", it).add(page) + } + } + + fun clientLanguage(clientLanguage: String) = + clientLanguage(JsonField.of(clientLanguage)) + + /** + * Sets [Builder.clientLanguage] to an arbitrary JSON value. + * + * You should usually call [Builder.clientLanguage] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun clientLanguage(clientLanguage: JsonField) = apply { + this.clientLanguage = clientLanguage + } + + 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 [Data]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .pages() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Data = + Data( + checkRequired("pages", pages).map { it.toImmutable() }, + clientLanguage, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Data = apply { + if (validated) { + return@apply + } + + pages().forEach { it.validate() } + clientLanguage() + 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 = + (pages.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + (if (clientLanguage.asKnown().isPresent) 1 else 0) + + class Page + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val actions: JsonField>, + private val duration: JsonField, + private val timestamp: JsonField, + private val url: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("actions") + @ExcludeMissing + actions: JsonField> = JsonMissing.of(), + @JsonProperty("duration") + @ExcludeMissing + duration: JsonField = JsonMissing.of(), + @JsonProperty("timestamp") + @ExcludeMissing + timestamp: JsonField = JsonMissing.of(), + @JsonProperty("url") @ExcludeMissing url: JsonField = JsonMissing.of(), + ) : this(actions, duration, timestamp, url, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun actions(): List = actions.getRequired("actions") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun duration(): Double = duration.getRequired("duration") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun timestamp(): Double = timestamp.getRequired("timestamp") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun url(): String = url.getRequired("url") + + /** + * Returns the raw JSON value of [actions]. + * + * Unlike [actions], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("actions") + @ExcludeMissing + fun _actions(): JsonField> = actions + + /** + * Returns the raw JSON value of [duration]. + * + * Unlike [duration], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("duration") @ExcludeMissing fun _duration(): JsonField = duration + + /** + * Returns the raw JSON value of [timestamp]. + * + * Unlike [timestamp], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("timestamp") + @ExcludeMissing + fun _timestamp(): JsonField = timestamp + + /** + * Returns the raw JSON value of [url]. + * + * Unlike [url], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("url") @ExcludeMissing fun _url(): JsonField = url + + @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 [Page]. + * + * The following fields are required: + * ```java + * .actions() + * .duration() + * .timestamp() + * .url() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Page]. */ + class Builder internal constructor() { + + private var actions: JsonField>? = null + private var duration: JsonField? = null + private var timestamp: JsonField? = null + private var url: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(page: Page) = apply { + actions = page.actions.map { it.toMutableList() } + duration = page.duration + timestamp = page.timestamp + url = page.url + additionalProperties = page.additionalProperties.toMutableMap() + } + + fun actions(actions: List) = actions(JsonField.of(actions)) + + /** + * Sets [Builder.actions] to an arbitrary JSON value. + * + * You should usually call [Builder.actions] with a well-typed `List` value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun actions(actions: JsonField>) = apply { + this.actions = actions.map { it.toMutableList() } + } + + /** + * Adds a single [Action] to [actions]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addAction(action: Action) = apply { + actions = + (actions ?: JsonField.of(mutableListOf())).also { + checkKnown("actions", it).add(action) + } + } + + fun duration(duration: Double) = duration(JsonField.of(duration)) + + /** + * Sets [Builder.duration] to an arbitrary JSON value. + * + * You should usually call [Builder.duration] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun duration(duration: JsonField) = apply { this.duration = duration } + + fun timestamp(timestamp: Double) = timestamp(JsonField.of(timestamp)) + + /** + * Sets [Builder.timestamp] to an arbitrary JSON value. + * + * You should usually call [Builder.timestamp] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun timestamp(timestamp: JsonField) = apply { this.timestamp = timestamp } + + fun url(url: String) = url(JsonField.of(url)) + + /** + * Sets [Builder.url] to an arbitrary JSON value. + * + * You should usually call [Builder.url] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun url(url: JsonField) = apply { this.url = url } + + 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 [Page]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .actions() + * .duration() + * .timestamp() + * .url() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Page = + Page( + checkRequired("actions", actions).map { it.toImmutable() }, + checkRequired("duration", duration), + checkRequired("timestamp", timestamp), + checkRequired("url", url), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Page = apply { + if (validated) { + return@apply + } + + actions().forEach { it.validate() } + duration() + timestamp() + url() + 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 = + (actions.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + (if (duration.asKnown().isPresent) 1 else 0) + + (if (timestamp.asKnown().isPresent) 1 else 0) + + (if (url.asKnown().isPresent) 1 else 0) + + class Action + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val method: JsonField, + private val parameters: JsonField, + private val result: JsonField, + private val timestamp: JsonField, + private val endTime: JsonField, + private val tokenUsage: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("method") + @ExcludeMissing + method: JsonField = JsonMissing.of(), + @JsonProperty("parameters") + @ExcludeMissing + parameters: JsonField = JsonMissing.of(), + @JsonProperty("result") + @ExcludeMissing + result: JsonField = JsonMissing.of(), + @JsonProperty("timestamp") + @ExcludeMissing + timestamp: JsonField = JsonMissing.of(), + @JsonProperty("endTime") + @ExcludeMissing + endTime: JsonField = JsonMissing.of(), + @JsonProperty("tokenUsage") + @ExcludeMissing + tokenUsage: JsonField = JsonMissing.of(), + ) : this(method, parameters, result, timestamp, endTime, tokenUsage, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun method(): String = method.getRequired("method") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun parameters(): Parameters = parameters.getRequired("parameters") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun result(): Result = result.getRequired("result") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun timestamp(): Double = timestamp.getRequired("timestamp") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun endTime(): Optional = endTime.getOptional("endTime") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun tokenUsage(): Optional = tokenUsage.getOptional("tokenUsage") + + /** + * Returns the raw JSON value of [method]. + * + * Unlike [method], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("method") @ExcludeMissing fun _method(): JsonField = method + + /** + * Returns the raw JSON value of [parameters]. + * + * Unlike [parameters], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("parameters") + @ExcludeMissing + fun _parameters(): JsonField = parameters + + /** + * Returns the raw JSON value of [result]. + * + * Unlike [result], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("result") @ExcludeMissing fun _result(): JsonField = result + + /** + * Returns the raw JSON value of [timestamp]. + * + * Unlike [timestamp], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("timestamp") + @ExcludeMissing + fun _timestamp(): JsonField = timestamp + + /** + * Returns the raw JSON value of [endTime]. + * + * Unlike [endTime], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("endTime") @ExcludeMissing fun _endTime(): JsonField = endTime + + /** + * Returns the raw JSON value of [tokenUsage]. + * + * Unlike [tokenUsage], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("tokenUsage") + @ExcludeMissing + fun _tokenUsage(): JsonField = tokenUsage + + @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 [Action]. + * + * The following fields are required: + * ```java + * .method() + * .parameters() + * .result() + * .timestamp() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Action]. */ + class Builder internal constructor() { + + private var method: JsonField? = null + private var parameters: JsonField? = null + private var result: JsonField? = null + private var timestamp: JsonField? = null + private var endTime: JsonField = JsonMissing.of() + private var tokenUsage: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(action: Action) = apply { + method = action.method + parameters = action.parameters + result = action.result + timestamp = action.timestamp + endTime = action.endTime + tokenUsage = action.tokenUsage + additionalProperties = action.additionalProperties.toMutableMap() + } + + fun method(method: String) = method(JsonField.of(method)) + + /** + * Sets [Builder.method] to an arbitrary JSON value. + * + * You should usually call [Builder.method] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun method(method: JsonField) = apply { this.method = method } + + fun parameters(parameters: Parameters) = parameters(JsonField.of(parameters)) + + /** + * Sets [Builder.parameters] to an arbitrary JSON value. + * + * You should usually call [Builder.parameters] with a well-typed [Parameters] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun parameters(parameters: JsonField) = apply { + this.parameters = parameters + } + + fun result(result: Result) = result(JsonField.of(result)) + + /** + * Sets [Builder.result] to an arbitrary JSON value. + * + * You should usually call [Builder.result] with a well-typed [Result] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun result(result: JsonField) = apply { this.result = result } + + fun timestamp(timestamp: Double) = timestamp(JsonField.of(timestamp)) + + /** + * Sets [Builder.timestamp] to an arbitrary JSON value. + * + * You should usually call [Builder.timestamp] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun timestamp(timestamp: JsonField) = apply { + this.timestamp = timestamp + } + + fun endTime(endTime: Double) = endTime(JsonField.of(endTime)) + + /** + * Sets [Builder.endTime] to an arbitrary JSON value. + * + * You should usually call [Builder.endTime] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun endTime(endTime: JsonField) = apply { this.endTime = endTime } + + fun tokenUsage(tokenUsage: TokenUsage) = tokenUsage(JsonField.of(tokenUsage)) + + /** + * Sets [Builder.tokenUsage] to an arbitrary JSON value. + * + * You should usually call [Builder.tokenUsage] with a well-typed [TokenUsage] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun tokenUsage(tokenUsage: JsonField) = apply { + this.tokenUsage = tokenUsage + } + + 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 [Action]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .method() + * .parameters() + * .result() + * .timestamp() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Action = + Action( + checkRequired("method", method), + checkRequired("parameters", parameters), + checkRequired("result", result), + checkRequired("timestamp", timestamp), + endTime, + tokenUsage, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Action = apply { + if (validated) { + return@apply + } + + method() + parameters().validate() + result().validate() + timestamp() + endTime() + tokenUsage().ifPresent { it.validate() } + 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 = + (if (method.asKnown().isPresent) 1 else 0) + + (parameters.asKnown().getOrNull()?.validity() ?: 0) + + (result.asKnown().getOrNull()?.validity() ?: 0) + + (if (timestamp.asKnown().isPresent) 1 else 0) + + (if (endTime.asKnown().isPresent) 1 else 0) + + (tokenUsage.asKnown().getOrNull()?.validity() ?: 0) + + class Parameters + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [Parameters]. + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Parameters]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = + mutableMapOf() + + @JvmSynthetic + internal fun from(parameters: Parameters) = apply { + additionalProperties = parameters.additionalProperties.toMutableMap() + } + + 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 [Parameters]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Parameters = Parameters(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): Parameters = 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 = + additionalProperties.count { (_, value) -> + !value.isNull() && !value.isMissing() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Parameters && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Parameters{additionalProperties=$additionalProperties}" + } + + class Result + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Result]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Result]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = + mutableMapOf() + + @JvmSynthetic + internal fun from(result: Result) = apply { + additionalProperties = result.additionalProperties.toMutableMap() + } + + 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 [Result]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Result = Result(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): Result = 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 = + additionalProperties.count { (_, value) -> + !value.isNull() && !value.isMissing() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Result && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Result{additionalProperties=$additionalProperties}" + } + + class TokenUsage + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val cost: JsonField, + private val inputTokens: JsonField, + private val outputTokens: JsonField, + private val timeMs: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("cost") + @ExcludeMissing + cost: JsonField = JsonMissing.of(), + @JsonProperty("inputTokens") + @ExcludeMissing + inputTokens: JsonField = JsonMissing.of(), + @JsonProperty("outputTokens") + @ExcludeMissing + outputTokens: JsonField = JsonMissing.of(), + @JsonProperty("timeMs") + @ExcludeMissing + timeMs: JsonField = JsonMissing.of(), + ) : this(cost, inputTokens, outputTokens, timeMs, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun cost(): Optional = cost.getOptional("cost") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun inputTokens(): Optional = inputTokens.getOptional("inputTokens") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun outputTokens(): Optional = outputTokens.getOptional("outputTokens") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun timeMs(): Optional = timeMs.getOptional("timeMs") + + /** + * Returns the raw JSON value of [cost]. + * + * Unlike [cost], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("cost") @ExcludeMissing fun _cost(): JsonField = cost + + /** + * Returns the raw JSON value of [inputTokens]. + * + * Unlike [inputTokens], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("inputTokens") + @ExcludeMissing + fun _inputTokens(): JsonField = inputTokens + + /** + * Returns the raw JSON value of [outputTokens]. + * + * Unlike [outputTokens], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("outputTokens") + @ExcludeMissing + fun _outputTokens(): JsonField = outputTokens + + /** + * Returns the raw JSON value of [timeMs]. + * + * Unlike [timeMs], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("timeMs") + @ExcludeMissing + fun _timeMs(): JsonField = timeMs + + @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 [TokenUsage]. + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [TokenUsage]. */ + class Builder internal constructor() { + + private var cost: JsonField = JsonMissing.of() + private var inputTokens: JsonField = JsonMissing.of() + private var outputTokens: JsonField = JsonMissing.of() + private var timeMs: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = + mutableMapOf() + + @JvmSynthetic + internal fun from(tokenUsage: TokenUsage) = apply { + cost = tokenUsage.cost + inputTokens = tokenUsage.inputTokens + outputTokens = tokenUsage.outputTokens + timeMs = tokenUsage.timeMs + additionalProperties = tokenUsage.additionalProperties.toMutableMap() + } + + fun cost(cost: Double) = cost(JsonField.of(cost)) + + /** + * Sets [Builder.cost] to an arbitrary JSON value. + * + * You should usually call [Builder.cost] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun cost(cost: JsonField) = apply { this.cost = cost } + + fun inputTokens(inputTokens: Double) = + inputTokens(JsonField.of(inputTokens)) + + /** + * Sets [Builder.inputTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.inputTokens] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun inputTokens(inputTokens: JsonField) = apply { + this.inputTokens = inputTokens + } + + fun outputTokens(outputTokens: Double) = + outputTokens(JsonField.of(outputTokens)) + + /** + * Sets [Builder.outputTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.outputTokens] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun outputTokens(outputTokens: JsonField) = apply { + this.outputTokens = outputTokens + } + + fun timeMs(timeMs: Double) = timeMs(JsonField.of(timeMs)) + + /** + * Sets [Builder.timeMs] to an arbitrary JSON value. + * + * You should usually call [Builder.timeMs] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun timeMs(timeMs: JsonField) = apply { this.timeMs = timeMs } + + 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 [TokenUsage]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): TokenUsage = + TokenUsage( + cost, + inputTokens, + outputTokens, + timeMs, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): TokenUsage = apply { + if (validated) { + return@apply + } + + cost() + inputTokens() + outputTokens() + timeMs() + 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 = + (if (cost.asKnown().isPresent) 1 else 0) + + (if (inputTokens.asKnown().isPresent) 1 else 0) + + (if (outputTokens.asKnown().isPresent) 1 else 0) + + (if (timeMs.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is TokenUsage && + cost == other.cost && + inputTokens == other.inputTokens && + outputTokens == other.outputTokens && + timeMs == other.timeMs && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(cost, inputTokens, outputTokens, timeMs, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "TokenUsage{cost=$cost, inputTokens=$inputTokens, outputTokens=$outputTokens, timeMs=$timeMs, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Action && + method == other.method && + parameters == other.parameters && + result == other.result && + timestamp == other.timestamp && + endTime == other.endTime && + tokenUsage == other.tokenUsage && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + method, + parameters, + result, + timestamp, + endTime, + tokenUsage, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Action{method=$method, parameters=$parameters, result=$result, timestamp=$timestamp, endTime=$endTime, tokenUsage=$tokenUsage, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Page && + actions == other.actions && + duration == other.duration && + timestamp == other.timestamp && + url == other.url && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(actions, duration, timestamp, url, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Page{actions=$actions, duration=$duration, timestamp=$timestamp, url=$url, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Data && + pages == other.pages && + clientLanguage == other.clientLanguage && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(pages, clientLanguage, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Data{pages=$pages, clientLanguage=$clientLanguage, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionReplayResponse && + data == other.data && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(data, success, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "SessionReplayResponse{data=$data, success=$success, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartParams.kt new file mode 100644 index 0000000..7d6d521 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartParams.kt @@ -0,0 +1,8159 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.BaseDeserializer +import com.browserbase.api.core.BaseSerializer +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.allMaxBy +import com.browserbase.api.core.checkKnown +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.getOrThrow +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 com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import java.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** + * Creates a new browser session with the specified configuration. Returns a session ID used for all + * subsequent operations. + */ +class SessionStartParams +private constructor( + private val xStreamResponse: XStreamResponse?, + private val body: Body, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, +) : Params { + + /** Whether to stream the response via SSE */ + fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) + + /** + * Model name to use for AI operations + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun modelName(): String = body.modelName() + + /** + * Timeout in ms for act operations (deprecated, v2 only) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun actTimeoutMs(): Optional = body.actTimeoutMs() + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun browser(): Optional = body.browser() + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun browserbaseSessionCreateParams(): Optional = + body.browserbaseSessionCreateParams() + + /** + * Existing Browserbase session ID to resume + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun browserbaseSessionId(): Optional = body.browserbaseSessionId() + + /** + * Timeout in ms to wait for DOM to settle + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun domSettleTimeoutMs(): Optional = body.domSettleTimeoutMs() + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun experimental(): Optional = body.experimental() + + /** + * Enable self-healing for failed actions + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun selfHeal(): Optional = body.selfHeal() + + /** + * Custom system prompt for AI operations + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun systemPrompt(): Optional = body.systemPrompt() + + /** + * Logging verbosity level (0=quiet, 1=normal, 2=debug) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun verbose(): Optional = body.verbose() + + /** + * Wait for captcha solves (deprecated, v2 only) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun waitForCaptchaSolves(): Optional = body.waitForCaptchaSolves() + + /** + * Returns the raw JSON value of [modelName]. + * + * Unlike [modelName], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _modelName(): JsonField = body._modelName() + + /** + * Returns the raw JSON value of [actTimeoutMs]. + * + * Unlike [actTimeoutMs], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _actTimeoutMs(): JsonField = body._actTimeoutMs() + + /** + * Returns the raw JSON value of [browser]. + * + * Unlike [browser], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _browser(): JsonField = body._browser() + + /** + * Returns the raw JSON value of [browserbaseSessionCreateParams]. + * + * Unlike [browserbaseSessionCreateParams], this method doesn't throw if the JSON field has an + * unexpected type. + */ + fun _browserbaseSessionCreateParams(): JsonField = + body._browserbaseSessionCreateParams() + + /** + * Returns the raw JSON value of [browserbaseSessionId]. + * + * Unlike [browserbaseSessionId], this method doesn't throw if the JSON field has an unexpected + * type. + */ + fun _browserbaseSessionId(): JsonField = body._browserbaseSessionId() + + /** + * Returns the raw JSON value of [domSettleTimeoutMs]. + * + * Unlike [domSettleTimeoutMs], this method doesn't throw if the JSON field has an unexpected + * type. + */ + fun _domSettleTimeoutMs(): JsonField = body._domSettleTimeoutMs() + + /** + * Returns the raw JSON value of [experimental]. + * + * Unlike [experimental], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _experimental(): JsonField = body._experimental() + + /** + * Returns the raw JSON value of [selfHeal]. + * + * Unlike [selfHeal], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _selfHeal(): JsonField = body._selfHeal() + + /** + * Returns the raw JSON value of [systemPrompt]. + * + * Unlike [systemPrompt], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _systemPrompt(): JsonField = body._systemPrompt() + + /** + * Returns the raw JSON value of [verbose]. + * + * Unlike [verbose], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _verbose(): JsonField = body._verbose() + + /** + * Returns the raw JSON value of [waitForCaptchaSolves]. + * + * Unlike [waitForCaptchaSolves], this method doesn't throw if the JSON field has an unexpected + * type. + */ + fun _waitForCaptchaSolves(): JsonField = body._waitForCaptchaSolves() + + fun _additionalBodyProperties(): Map = body._additionalProperties() + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [SessionStartParams]. + * + * The following fields are required: + * ```java + * .modelName() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionStartParams]. */ + class Builder internal constructor() { + + 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() + + @JvmSynthetic + internal fun from(sessionStartParams: SessionStartParams) = apply { + xStreamResponse = sessionStartParams.xStreamResponse + body = sessionStartParams.body.toBuilder() + additionalHeaders = sessionStartParams.additionalHeaders.toBuilder() + additionalQueryParams = sessionStartParams.additionalQueryParams.toBuilder() + } + + /** Whether to stream the response via SSE */ + fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { + this.xStreamResponse = xStreamResponse + } + + /** Alias for calling [Builder.xStreamResponse] with `xStreamResponse.orElse(null)`. */ + 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: + * - [modelName] + * - [actTimeoutMs] + * - [browser] + * - [browserbaseSessionCreateParams] + * - [browserbaseSessionId] + * - etc. + */ + fun body(body: Body) = apply { this.body = body.toBuilder() } + + /** Model name to use for AI operations */ + fun modelName(modelName: String) = apply { body.modelName(modelName) } + + /** + * Sets [Builder.modelName] to an arbitrary JSON value. + * + * You should usually call [Builder.modelName] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun modelName(modelName: JsonField) = apply { body.modelName(modelName) } + + /** Timeout in ms for act operations (deprecated, v2 only) */ + fun actTimeoutMs(actTimeoutMs: Double) = apply { body.actTimeoutMs(actTimeoutMs) } + + /** + * Sets [Builder.actTimeoutMs] to an arbitrary JSON value. + * + * You should usually call [Builder.actTimeoutMs] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun actTimeoutMs(actTimeoutMs: JsonField) = apply { + body.actTimeoutMs(actTimeoutMs) + } + + fun browser(browser: Browser) = apply { body.browser(browser) } + + /** + * Sets [Builder.browser] to an arbitrary JSON value. + * + * You should usually call [Builder.browser] with a well-typed [Browser] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun browser(browser: JsonField) = apply { body.browser(browser) } + + fun browserbaseSessionCreateParams( + browserbaseSessionCreateParams: BrowserbaseSessionCreateParams + ) = apply { body.browserbaseSessionCreateParams(browserbaseSessionCreateParams) } + + /** + * Sets [Builder.browserbaseSessionCreateParams] to an arbitrary JSON value. + * + * You should usually call [Builder.browserbaseSessionCreateParams] with a well-typed + * [BrowserbaseSessionCreateParams] value instead. This method is primarily for setting the + * field to an undocumented or not yet supported value. + */ + fun browserbaseSessionCreateParams( + browserbaseSessionCreateParams: JsonField + ) = apply { body.browserbaseSessionCreateParams(browserbaseSessionCreateParams) } + + /** Existing Browserbase session ID to resume */ + fun browserbaseSessionId(browserbaseSessionId: String) = apply { + body.browserbaseSessionId(browserbaseSessionId) + } + + /** + * Sets [Builder.browserbaseSessionId] to an arbitrary JSON value. + * + * You should usually call [Builder.browserbaseSessionId] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun browserbaseSessionId(browserbaseSessionId: JsonField) = apply { + body.browserbaseSessionId(browserbaseSessionId) + } + + /** Timeout in ms to wait for DOM to settle */ + fun domSettleTimeoutMs(domSettleTimeoutMs: Double) = apply { + body.domSettleTimeoutMs(domSettleTimeoutMs) + } + + /** + * Sets [Builder.domSettleTimeoutMs] to an arbitrary JSON value. + * + * You should usually call [Builder.domSettleTimeoutMs] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun domSettleTimeoutMs(domSettleTimeoutMs: JsonField) = apply { + body.domSettleTimeoutMs(domSettleTimeoutMs) + } + + fun experimental(experimental: Boolean) = apply { body.experimental(experimental) } + + /** + * Sets [Builder.experimental] to an arbitrary JSON value. + * + * You should usually call [Builder.experimental] with a well-typed [Boolean] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun experimental(experimental: JsonField) = apply { + body.experimental(experimental) + } + + /** Enable self-healing for failed actions */ + fun selfHeal(selfHeal: Boolean) = apply { body.selfHeal(selfHeal) } + + /** + * Sets [Builder.selfHeal] to an arbitrary JSON value. + * + * You should usually call [Builder.selfHeal] with a well-typed [Boolean] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun selfHeal(selfHeal: JsonField) = apply { body.selfHeal(selfHeal) } + + /** Custom system prompt for AI operations */ + fun systemPrompt(systemPrompt: String) = apply { body.systemPrompt(systemPrompt) } + + /** + * Sets [Builder.systemPrompt] to an arbitrary JSON value. + * + * You should usually call [Builder.systemPrompt] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun systemPrompt(systemPrompt: JsonField) = apply { + body.systemPrompt(systemPrompt) + } + + /** Logging verbosity level (0=quiet, 1=normal, 2=debug) */ + fun verbose(verbose: Verbose) = apply { body.verbose(verbose) } + + /** + * Sets [Builder.verbose] to an arbitrary JSON value. + * + * You should usually call [Builder.verbose] with a well-typed [Verbose] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun verbose(verbose: JsonField) = apply { body.verbose(verbose) } + + /** Wait for captcha solves (deprecated, v2 only) */ + fun waitForCaptchaSolves(waitForCaptchaSolves: Boolean) = apply { + body.waitForCaptchaSolves(waitForCaptchaSolves) + } + + /** + * Sets [Builder.waitForCaptchaSolves] to an arbitrary JSON value. + * + * You should usually call [Builder.waitForCaptchaSolves] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun waitForCaptchaSolves(waitForCaptchaSolves: JsonField) = apply { + body.waitForCaptchaSolves(waitForCaptchaSolves) + } + + 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) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + additionalQueryParams.removeAll(keys) + } + + /** + * Returns an immutable instance of [SessionStartParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .modelName() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionStartParams = + SessionStartParams( + xStreamResponse, + body.build(), + additionalHeaders.build(), + additionalQueryParams.build(), + ) + } + + fun _body(): Body = body + + override fun _headers(): Headers = + Headers.builder() + .apply { + xStreamResponse?.let { put("x-stream-response", it.toString()) } + putAll(additionalHeaders) + } + .build() + + override fun _queryParams(): QueryParams = additionalQueryParams + + class Body + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val modelName: JsonField, + private val actTimeoutMs: JsonField, + private val browser: JsonField, + private val browserbaseSessionCreateParams: JsonField, + private val browserbaseSessionId: JsonField, + private val domSettleTimeoutMs: JsonField, + private val experimental: JsonField, + private val selfHeal: JsonField, + private val systemPrompt: JsonField, + private val verbose: JsonField, + private val waitForCaptchaSolves: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("modelName") + @ExcludeMissing + modelName: JsonField = JsonMissing.of(), + @JsonProperty("actTimeoutMs") + @ExcludeMissing + actTimeoutMs: JsonField = JsonMissing.of(), + @JsonProperty("browser") @ExcludeMissing browser: JsonField = JsonMissing.of(), + @JsonProperty("browserbaseSessionCreateParams") + @ExcludeMissing + browserbaseSessionCreateParams: JsonField = + JsonMissing.of(), + @JsonProperty("browserbaseSessionID") + @ExcludeMissing + browserbaseSessionId: JsonField = JsonMissing.of(), + @JsonProperty("domSettleTimeoutMs") + @ExcludeMissing + domSettleTimeoutMs: JsonField = JsonMissing.of(), + @JsonProperty("experimental") + @ExcludeMissing + experimental: JsonField = JsonMissing.of(), + @JsonProperty("selfHeal") + @ExcludeMissing + selfHeal: JsonField = JsonMissing.of(), + @JsonProperty("systemPrompt") + @ExcludeMissing + systemPrompt: JsonField = JsonMissing.of(), + @JsonProperty("verbose") @ExcludeMissing verbose: JsonField = JsonMissing.of(), + @JsonProperty("waitForCaptchaSolves") + @ExcludeMissing + waitForCaptchaSolves: JsonField = JsonMissing.of(), + ) : this( + modelName, + actTimeoutMs, + browser, + browserbaseSessionCreateParams, + browserbaseSessionId, + domSettleTimeoutMs, + experimental, + selfHeal, + systemPrompt, + verbose, + waitForCaptchaSolves, + mutableMapOf(), + ) + + /** + * Model name to use for AI operations + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun modelName(): String = modelName.getRequired("modelName") + + /** + * Timeout in ms for act operations (deprecated, v2 only) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun actTimeoutMs(): Optional = actTimeoutMs.getOptional("actTimeoutMs") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun browser(): Optional = browser.getOptional("browser") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun browserbaseSessionCreateParams(): Optional = + browserbaseSessionCreateParams.getOptional("browserbaseSessionCreateParams") + + /** + * Existing Browserbase session ID to resume + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun browserbaseSessionId(): Optional = + browserbaseSessionId.getOptional("browserbaseSessionID") + + /** + * Timeout in ms to wait for DOM to settle + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun domSettleTimeoutMs(): Optional = + domSettleTimeoutMs.getOptional("domSettleTimeoutMs") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun experimental(): Optional = experimental.getOptional("experimental") + + /** + * Enable self-healing for failed actions + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun selfHeal(): Optional = selfHeal.getOptional("selfHeal") + + /** + * Custom system prompt for AI operations + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun systemPrompt(): Optional = systemPrompt.getOptional("systemPrompt") + + /** + * Logging verbosity level (0=quiet, 1=normal, 2=debug) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun verbose(): Optional = verbose.getOptional("verbose") + + /** + * Wait for captcha solves (deprecated, v2 only) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun waitForCaptchaSolves(): Optional = + waitForCaptchaSolves.getOptional("waitForCaptchaSolves") + + /** + * Returns the raw JSON value of [modelName]. + * + * Unlike [modelName], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("modelName") @ExcludeMissing fun _modelName(): JsonField = modelName + + /** + * Returns the raw JSON value of [actTimeoutMs]. + * + * Unlike [actTimeoutMs], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("actTimeoutMs") + @ExcludeMissing + fun _actTimeoutMs(): JsonField = actTimeoutMs + + /** + * Returns the raw JSON value of [browser]. + * + * Unlike [browser], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("browser") @ExcludeMissing fun _browser(): JsonField = browser + + /** + * Returns the raw JSON value of [browserbaseSessionCreateParams]. + * + * Unlike [browserbaseSessionCreateParams], this method doesn't throw if the JSON field has + * an unexpected type. + */ + @JsonProperty("browserbaseSessionCreateParams") + @ExcludeMissing + fun _browserbaseSessionCreateParams(): JsonField = + browserbaseSessionCreateParams + + /** + * Returns the raw JSON value of [browserbaseSessionId]. + * + * Unlike [browserbaseSessionId], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("browserbaseSessionID") + @ExcludeMissing + fun _browserbaseSessionId(): JsonField = browserbaseSessionId + + /** + * Returns the raw JSON value of [domSettleTimeoutMs]. + * + * Unlike [domSettleTimeoutMs], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("domSettleTimeoutMs") + @ExcludeMissing + fun _domSettleTimeoutMs(): JsonField = domSettleTimeoutMs + + /** + * Returns the raw JSON value of [experimental]. + * + * Unlike [experimental], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("experimental") + @ExcludeMissing + fun _experimental(): JsonField = experimental + + /** + * Returns the raw JSON value of [selfHeal]. + * + * Unlike [selfHeal], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("selfHeal") @ExcludeMissing fun _selfHeal(): JsonField = selfHeal + + /** + * Returns the raw JSON value of [systemPrompt]. + * + * Unlike [systemPrompt], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("systemPrompt") + @ExcludeMissing + fun _systemPrompt(): JsonField = systemPrompt + + /** + * Returns the raw JSON value of [verbose]. + * + * Unlike [verbose], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("verbose") @ExcludeMissing fun _verbose(): JsonField = verbose + + /** + * Returns the raw JSON value of [waitForCaptchaSolves]. + * + * Unlike [waitForCaptchaSolves], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("waitForCaptchaSolves") + @ExcludeMissing + fun _waitForCaptchaSolves(): JsonField = waitForCaptchaSolves + + @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]. + * + * The following fields are required: + * ```java + * .modelName() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Body]. */ + class Builder internal constructor() { + + private var modelName: JsonField? = null + private var actTimeoutMs: JsonField = JsonMissing.of() + private var browser: JsonField = JsonMissing.of() + private var browserbaseSessionCreateParams: JsonField = + JsonMissing.of() + private var browserbaseSessionId: JsonField = JsonMissing.of() + private var domSettleTimeoutMs: JsonField = JsonMissing.of() + private var experimental: JsonField = JsonMissing.of() + private var selfHeal: JsonField = JsonMissing.of() + private var systemPrompt: JsonField = JsonMissing.of() + private var verbose: JsonField = JsonMissing.of() + private var waitForCaptchaSolves: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(body: Body) = apply { + modelName = body.modelName + actTimeoutMs = body.actTimeoutMs + browser = body.browser + browserbaseSessionCreateParams = body.browserbaseSessionCreateParams + browserbaseSessionId = body.browserbaseSessionId + domSettleTimeoutMs = body.domSettleTimeoutMs + experimental = body.experimental + selfHeal = body.selfHeal + systemPrompt = body.systemPrompt + verbose = body.verbose + waitForCaptchaSolves = body.waitForCaptchaSolves + additionalProperties = body.additionalProperties.toMutableMap() + } + + /** Model name to use for AI operations */ + fun modelName(modelName: String) = modelName(JsonField.of(modelName)) + + /** + * Sets [Builder.modelName] to an arbitrary JSON value. + * + * You should usually call [Builder.modelName] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun modelName(modelName: JsonField) = apply { this.modelName = modelName } + + /** Timeout in ms for act operations (deprecated, v2 only) */ + fun actTimeoutMs(actTimeoutMs: Double) = actTimeoutMs(JsonField.of(actTimeoutMs)) + + /** + * Sets [Builder.actTimeoutMs] to an arbitrary JSON value. + * + * You should usually call [Builder.actTimeoutMs] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun actTimeoutMs(actTimeoutMs: JsonField) = apply { + this.actTimeoutMs = actTimeoutMs + } + + fun browser(browser: Browser) = browser(JsonField.of(browser)) + + /** + * Sets [Builder.browser] to an arbitrary JSON value. + * + * You should usually call [Builder.browser] with a well-typed [Browser] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun browser(browser: JsonField) = apply { this.browser = browser } + + fun browserbaseSessionCreateParams( + browserbaseSessionCreateParams: BrowserbaseSessionCreateParams + ) = browserbaseSessionCreateParams(JsonField.of(browserbaseSessionCreateParams)) + + /** + * Sets [Builder.browserbaseSessionCreateParams] to an arbitrary JSON value. + * + * You should usually call [Builder.browserbaseSessionCreateParams] with a well-typed + * [BrowserbaseSessionCreateParams] value instead. This method is primarily for setting + * the field to an undocumented or not yet supported value. + */ + fun browserbaseSessionCreateParams( + browserbaseSessionCreateParams: JsonField + ) = apply { this.browserbaseSessionCreateParams = browserbaseSessionCreateParams } + + /** Existing Browserbase session ID to resume */ + fun browserbaseSessionId(browserbaseSessionId: String) = + browserbaseSessionId(JsonField.of(browserbaseSessionId)) + + /** + * Sets [Builder.browserbaseSessionId] to an arbitrary JSON value. + * + * You should usually call [Builder.browserbaseSessionId] with a well-typed [String] + * value instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun browserbaseSessionId(browserbaseSessionId: JsonField) = apply { + this.browserbaseSessionId = browserbaseSessionId + } + + /** Timeout in ms to wait for DOM to settle */ + fun domSettleTimeoutMs(domSettleTimeoutMs: Double) = + domSettleTimeoutMs(JsonField.of(domSettleTimeoutMs)) + + /** + * Sets [Builder.domSettleTimeoutMs] to an arbitrary JSON value. + * + * You should usually call [Builder.domSettleTimeoutMs] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun domSettleTimeoutMs(domSettleTimeoutMs: JsonField) = apply { + this.domSettleTimeoutMs = domSettleTimeoutMs + } + + fun experimental(experimental: Boolean) = experimental(JsonField.of(experimental)) + + /** + * Sets [Builder.experimental] to an arbitrary JSON value. + * + * You should usually call [Builder.experimental] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun experimental(experimental: JsonField) = apply { + this.experimental = experimental + } + + /** Enable self-healing for failed actions */ + fun selfHeal(selfHeal: Boolean) = selfHeal(JsonField.of(selfHeal)) + + /** + * Sets [Builder.selfHeal] to an arbitrary JSON value. + * + * You should usually call [Builder.selfHeal] with a well-typed [Boolean] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun selfHeal(selfHeal: JsonField) = apply { this.selfHeal = selfHeal } + + /** Custom system prompt for AI operations */ + fun systemPrompt(systemPrompt: String) = systemPrompt(JsonField.of(systemPrompt)) + + /** + * Sets [Builder.systemPrompt] to an arbitrary JSON value. + * + * You should usually call [Builder.systemPrompt] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun systemPrompt(systemPrompt: JsonField) = apply { + this.systemPrompt = systemPrompt + } + + /** Logging verbosity level (0=quiet, 1=normal, 2=debug) */ + fun verbose(verbose: Verbose) = verbose(JsonField.of(verbose)) + + /** + * Sets [Builder.verbose] to an arbitrary JSON value. + * + * You should usually call [Builder.verbose] with a well-typed [Verbose] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun verbose(verbose: JsonField) = apply { this.verbose = verbose } + + /** Wait for captcha solves (deprecated, v2 only) */ + fun waitForCaptchaSolves(waitForCaptchaSolves: Boolean) = + waitForCaptchaSolves(JsonField.of(waitForCaptchaSolves)) + + /** + * Sets [Builder.waitForCaptchaSolves] to an arbitrary JSON value. + * + * You should usually call [Builder.waitForCaptchaSolves] with a well-typed [Boolean] + * value instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun waitForCaptchaSolves(waitForCaptchaSolves: JsonField) = apply { + this.waitForCaptchaSolves = waitForCaptchaSolves + } + + 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. + * + * The following fields are required: + * ```java + * .modelName() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Body = + Body( + checkRequired("modelName", modelName), + actTimeoutMs, + browser, + browserbaseSessionCreateParams, + browserbaseSessionId, + domSettleTimeoutMs, + experimental, + selfHeal, + systemPrompt, + verbose, + waitForCaptchaSolves, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Body = apply { + if (validated) { + return@apply + } + + modelName() + actTimeoutMs() + browser().ifPresent { it.validate() } + browserbaseSessionCreateParams().ifPresent { it.validate() } + browserbaseSessionId() + domSettleTimeoutMs() + experimental() + selfHeal() + systemPrompt() + verbose().ifPresent { it.validate() } + waitForCaptchaSolves() + 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 = + (if (modelName.asKnown().isPresent) 1 else 0) + + (if (actTimeoutMs.asKnown().isPresent) 1 else 0) + + (browser.asKnown().getOrNull()?.validity() ?: 0) + + (browserbaseSessionCreateParams.asKnown().getOrNull()?.validity() ?: 0) + + (if (browserbaseSessionId.asKnown().isPresent) 1 else 0) + + (if (domSettleTimeoutMs.asKnown().isPresent) 1 else 0) + + (if (experimental.asKnown().isPresent) 1 else 0) + + (if (selfHeal.asKnown().isPresent) 1 else 0) + + (if (systemPrompt.asKnown().isPresent) 1 else 0) + + (verbose.asKnown().getOrNull()?.validity() ?: 0) + + (if (waitForCaptchaSolves.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Body && + modelName == other.modelName && + actTimeoutMs == other.actTimeoutMs && + browser == other.browser && + browserbaseSessionCreateParams == other.browserbaseSessionCreateParams && + browserbaseSessionId == other.browserbaseSessionId && + domSettleTimeoutMs == other.domSettleTimeoutMs && + experimental == other.experimental && + selfHeal == other.selfHeal && + systemPrompt == other.systemPrompt && + verbose == other.verbose && + waitForCaptchaSolves == other.waitForCaptchaSolves && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + modelName, + actTimeoutMs, + browser, + browserbaseSessionCreateParams, + browserbaseSessionId, + domSettleTimeoutMs, + experimental, + selfHeal, + systemPrompt, + verbose, + waitForCaptchaSolves, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Body{modelName=$modelName, actTimeoutMs=$actTimeoutMs, browser=$browser, browserbaseSessionCreateParams=$browserbaseSessionCreateParams, browserbaseSessionId=$browserbaseSessionId, domSettleTimeoutMs=$domSettleTimeoutMs, experimental=$experimental, selfHeal=$selfHeal, systemPrompt=$systemPrompt, verbose=$verbose, waitForCaptchaSolves=$waitForCaptchaSolves, additionalProperties=$additionalProperties}" + } + + class Browser + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val cdpUrl: JsonField, + private val launchOptions: JsonField, + private val type: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("cdpUrl") @ExcludeMissing cdpUrl: JsonField = JsonMissing.of(), + @JsonProperty("launchOptions") + @ExcludeMissing + launchOptions: JsonField = JsonMissing.of(), + @JsonProperty("type") @ExcludeMissing type: JsonField = JsonMissing.of(), + ) : this(cdpUrl, launchOptions, type, mutableMapOf()) + + /** + * Chrome DevTools Protocol URL for connecting to existing browser + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun cdpUrl(): Optional = cdpUrl.getOptional("cdpUrl") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun launchOptions(): Optional = launchOptions.getOptional("launchOptions") + + /** + * Browser type to use + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun type(): Optional = type.getOptional("type") + + /** + * Returns the raw JSON value of [cdpUrl]. + * + * Unlike [cdpUrl], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("cdpUrl") @ExcludeMissing fun _cdpUrl(): JsonField = cdpUrl + + /** + * Returns the raw JSON value of [launchOptions]. + * + * Unlike [launchOptions], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("launchOptions") + @ExcludeMissing + fun _launchOptions(): JsonField = launchOptions + + /** + * Returns the raw JSON value of [type]. + * + * Unlike [type], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("type") @ExcludeMissing fun _type(): JsonField = type + + @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 [Browser]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Browser]. */ + class Builder internal constructor() { + + private var cdpUrl: JsonField = JsonMissing.of() + private var launchOptions: JsonField = JsonMissing.of() + private var type: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(browser: Browser) = apply { + cdpUrl = browser.cdpUrl + launchOptions = browser.launchOptions + type = browser.type + additionalProperties = browser.additionalProperties.toMutableMap() + } + + /** Chrome DevTools Protocol URL for connecting to existing browser */ + fun cdpUrl(cdpUrl: String) = cdpUrl(JsonField.of(cdpUrl)) + + /** + * Sets [Builder.cdpUrl] to an arbitrary JSON value. + * + * You should usually call [Builder.cdpUrl] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun cdpUrl(cdpUrl: JsonField) = apply { this.cdpUrl = cdpUrl } + + fun launchOptions(launchOptions: LaunchOptions) = + launchOptions(JsonField.of(launchOptions)) + + /** + * Sets [Builder.launchOptions] to an arbitrary JSON value. + * + * You should usually call [Builder.launchOptions] with a well-typed [LaunchOptions] + * value instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun launchOptions(launchOptions: JsonField) = apply { + this.launchOptions = launchOptions + } + + /** Browser type to use */ + fun type(type: Type) = type(JsonField.of(type)) + + /** + * Sets [Builder.type] to an arbitrary JSON value. + * + * You should usually call [Builder.type] with a well-typed [Type] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun type(type: JsonField) = apply { this.type = type } + + 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 [Browser]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Browser = + Browser(cdpUrl, launchOptions, type, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Browser = apply { + if (validated) { + return@apply + } + + cdpUrl() + launchOptions().ifPresent { it.validate() } + type().ifPresent { it.validate() } + 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 = + (if (cdpUrl.asKnown().isPresent) 1 else 0) + + (launchOptions.asKnown().getOrNull()?.validity() ?: 0) + + (type.asKnown().getOrNull()?.validity() ?: 0) + + class LaunchOptions + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val acceptDownloads: JsonField, + private val args: JsonField>, + private val cdpHeaders: JsonField, + private val cdpUrl: JsonField, + private val chromiumSandbox: JsonField, + private val connectTimeoutMs: JsonField, + private val deviceScaleFactor: JsonField, + private val devtools: JsonField, + private val downloadsPath: JsonField, + private val executablePath: JsonField, + private val hasTouch: JsonField, + private val headless: JsonField, + private val ignoreDefaultArgs: JsonField, + private val ignoreHttpsErrors: JsonField, + private val locale: JsonField, + private val port: JsonField, + private val preserveUserDataDir: JsonField, + private val proxy: JsonField, + private val userDataDir: JsonField, + private val viewport: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("acceptDownloads") + @ExcludeMissing + acceptDownloads: JsonField = JsonMissing.of(), + @JsonProperty("args") + @ExcludeMissing + args: JsonField> = JsonMissing.of(), + @JsonProperty("cdpHeaders") + @ExcludeMissing + cdpHeaders: JsonField = JsonMissing.of(), + @JsonProperty("cdpUrl") + @ExcludeMissing + cdpUrl: JsonField = JsonMissing.of(), + @JsonProperty("chromiumSandbox") + @ExcludeMissing + chromiumSandbox: JsonField = JsonMissing.of(), + @JsonProperty("connectTimeoutMs") + @ExcludeMissing + connectTimeoutMs: JsonField = JsonMissing.of(), + @JsonProperty("deviceScaleFactor") + @ExcludeMissing + deviceScaleFactor: JsonField = JsonMissing.of(), + @JsonProperty("devtools") + @ExcludeMissing + devtools: JsonField = JsonMissing.of(), + @JsonProperty("downloadsPath") + @ExcludeMissing + downloadsPath: JsonField = JsonMissing.of(), + @JsonProperty("executablePath") + @ExcludeMissing + executablePath: JsonField = JsonMissing.of(), + @JsonProperty("hasTouch") + @ExcludeMissing + hasTouch: JsonField = JsonMissing.of(), + @JsonProperty("headless") + @ExcludeMissing + headless: JsonField = JsonMissing.of(), + @JsonProperty("ignoreDefaultArgs") + @ExcludeMissing + ignoreDefaultArgs: JsonField = JsonMissing.of(), + @JsonProperty("ignoreHTTPSErrors") + @ExcludeMissing + ignoreHttpsErrors: JsonField = JsonMissing.of(), + @JsonProperty("locale") + @ExcludeMissing + locale: JsonField = JsonMissing.of(), + @JsonProperty("port") @ExcludeMissing port: JsonField = JsonMissing.of(), + @JsonProperty("preserveUserDataDir") + @ExcludeMissing + preserveUserDataDir: JsonField = JsonMissing.of(), + @JsonProperty("proxy") @ExcludeMissing proxy: JsonField = JsonMissing.of(), + @JsonProperty("userDataDir") + @ExcludeMissing + userDataDir: JsonField = JsonMissing.of(), + @JsonProperty("viewport") + @ExcludeMissing + viewport: JsonField = JsonMissing.of(), + ) : this( + acceptDownloads, + args, + cdpHeaders, + cdpUrl, + chromiumSandbox, + connectTimeoutMs, + deviceScaleFactor, + devtools, + downloadsPath, + executablePath, + hasTouch, + headless, + ignoreDefaultArgs, + ignoreHttpsErrors, + locale, + port, + preserveUserDataDir, + proxy, + userDataDir, + viewport, + mutableMapOf(), + ) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun acceptDownloads(): Optional = + acceptDownloads.getOptional("acceptDownloads") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun args(): Optional> = args.getOptional("args") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun cdpHeaders(): Optional = cdpHeaders.getOptional("cdpHeaders") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun cdpUrl(): Optional = cdpUrl.getOptional("cdpUrl") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun chromiumSandbox(): Optional = + chromiumSandbox.getOptional("chromiumSandbox") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun connectTimeoutMs(): Optional = + connectTimeoutMs.getOptional("connectTimeoutMs") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun deviceScaleFactor(): Optional = + deviceScaleFactor.getOptional("deviceScaleFactor") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun devtools(): Optional = devtools.getOptional("devtools") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun downloadsPath(): Optional = downloadsPath.getOptional("downloadsPath") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun executablePath(): Optional = executablePath.getOptional("executablePath") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun hasTouch(): Optional = hasTouch.getOptional("hasTouch") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun headless(): Optional = headless.getOptional("headless") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun ignoreDefaultArgs(): Optional = + ignoreDefaultArgs.getOptional("ignoreDefaultArgs") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun ignoreHttpsErrors(): Optional = + ignoreHttpsErrors.getOptional("ignoreHTTPSErrors") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun locale(): Optional = locale.getOptional("locale") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun port(): Optional = port.getOptional("port") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun preserveUserDataDir(): Optional = + preserveUserDataDir.getOptional("preserveUserDataDir") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun proxy(): Optional = proxy.getOptional("proxy") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun userDataDir(): Optional = userDataDir.getOptional("userDataDir") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun viewport(): Optional = viewport.getOptional("viewport") + + /** + * Returns the raw JSON value of [acceptDownloads]. + * + * Unlike [acceptDownloads], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("acceptDownloads") + @ExcludeMissing + fun _acceptDownloads(): JsonField = acceptDownloads + + /** + * Returns the raw JSON value of [args]. + * + * Unlike [args], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("args") @ExcludeMissing fun _args(): JsonField> = args + + /** + * Returns the raw JSON value of [cdpHeaders]. + * + * Unlike [cdpHeaders], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("cdpHeaders") + @ExcludeMissing + fun _cdpHeaders(): JsonField = cdpHeaders + + /** + * Returns the raw JSON value of [cdpUrl]. + * + * Unlike [cdpUrl], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("cdpUrl") @ExcludeMissing fun _cdpUrl(): JsonField = cdpUrl + + /** + * Returns the raw JSON value of [chromiumSandbox]. + * + * Unlike [chromiumSandbox], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("chromiumSandbox") + @ExcludeMissing + fun _chromiumSandbox(): JsonField = chromiumSandbox + + /** + * Returns the raw JSON value of [connectTimeoutMs]. + * + * Unlike [connectTimeoutMs], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("connectTimeoutMs") + @ExcludeMissing + fun _connectTimeoutMs(): JsonField = connectTimeoutMs + + /** + * Returns the raw JSON value of [deviceScaleFactor]. + * + * Unlike [deviceScaleFactor], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("deviceScaleFactor") + @ExcludeMissing + fun _deviceScaleFactor(): JsonField = deviceScaleFactor + + /** + * Returns the raw JSON value of [devtools]. + * + * Unlike [devtools], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("devtools") @ExcludeMissing fun _devtools(): JsonField = devtools + + /** + * Returns the raw JSON value of [downloadsPath]. + * + * Unlike [downloadsPath], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("downloadsPath") + @ExcludeMissing + fun _downloadsPath(): JsonField = downloadsPath + + /** + * Returns the raw JSON value of [executablePath]. + * + * Unlike [executablePath], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("executablePath") + @ExcludeMissing + fun _executablePath(): JsonField = executablePath + + /** + * Returns the raw JSON value of [hasTouch]. + * + * Unlike [hasTouch], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("hasTouch") @ExcludeMissing fun _hasTouch(): JsonField = hasTouch + + /** + * Returns the raw JSON value of [headless]. + * + * Unlike [headless], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("headless") @ExcludeMissing fun _headless(): JsonField = headless + + /** + * Returns the raw JSON value of [ignoreDefaultArgs]. + * + * Unlike [ignoreDefaultArgs], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("ignoreDefaultArgs") + @ExcludeMissing + fun _ignoreDefaultArgs(): JsonField = ignoreDefaultArgs + + /** + * Returns the raw JSON value of [ignoreHttpsErrors]. + * + * Unlike [ignoreHttpsErrors], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("ignoreHTTPSErrors") + @ExcludeMissing + fun _ignoreHttpsErrors(): JsonField = ignoreHttpsErrors + + /** + * Returns the raw JSON value of [locale]. + * + * Unlike [locale], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("locale") @ExcludeMissing fun _locale(): JsonField = locale + + /** + * Returns the raw JSON value of [port]. + * + * Unlike [port], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("port") @ExcludeMissing fun _port(): JsonField = port + + /** + * Returns the raw JSON value of [preserveUserDataDir]. + * + * Unlike [preserveUserDataDir], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("preserveUserDataDir") + @ExcludeMissing + fun _preserveUserDataDir(): JsonField = preserveUserDataDir + + /** + * Returns the raw JSON value of [proxy]. + * + * Unlike [proxy], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("proxy") @ExcludeMissing fun _proxy(): JsonField = proxy + + /** + * Returns the raw JSON value of [userDataDir]. + * + * Unlike [userDataDir], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("userDataDir") + @ExcludeMissing + fun _userDataDir(): JsonField = userDataDir + + /** + * Returns the raw JSON value of [viewport]. + * + * Unlike [viewport], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("viewport") + @ExcludeMissing + fun _viewport(): JsonField = viewport + + @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 [LaunchOptions]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [LaunchOptions]. */ + class Builder internal constructor() { + + private var acceptDownloads: JsonField = JsonMissing.of() + private var args: JsonField>? = null + private var cdpHeaders: JsonField = JsonMissing.of() + private var cdpUrl: JsonField = JsonMissing.of() + private var chromiumSandbox: JsonField = JsonMissing.of() + private var connectTimeoutMs: JsonField = JsonMissing.of() + private var deviceScaleFactor: JsonField = JsonMissing.of() + private var devtools: JsonField = JsonMissing.of() + private var downloadsPath: JsonField = JsonMissing.of() + private var executablePath: JsonField = JsonMissing.of() + private var hasTouch: JsonField = JsonMissing.of() + private var headless: JsonField = JsonMissing.of() + private var ignoreDefaultArgs: JsonField = JsonMissing.of() + private var ignoreHttpsErrors: JsonField = JsonMissing.of() + private var locale: JsonField = JsonMissing.of() + private var port: JsonField = JsonMissing.of() + private var preserveUserDataDir: JsonField = JsonMissing.of() + private var proxy: JsonField = JsonMissing.of() + private var userDataDir: JsonField = JsonMissing.of() + private var viewport: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(launchOptions: LaunchOptions) = apply { + acceptDownloads = launchOptions.acceptDownloads + args = launchOptions.args.map { it.toMutableList() } + cdpHeaders = launchOptions.cdpHeaders + cdpUrl = launchOptions.cdpUrl + chromiumSandbox = launchOptions.chromiumSandbox + connectTimeoutMs = launchOptions.connectTimeoutMs + deviceScaleFactor = launchOptions.deviceScaleFactor + devtools = launchOptions.devtools + downloadsPath = launchOptions.downloadsPath + executablePath = launchOptions.executablePath + hasTouch = launchOptions.hasTouch + headless = launchOptions.headless + ignoreDefaultArgs = launchOptions.ignoreDefaultArgs + ignoreHttpsErrors = launchOptions.ignoreHttpsErrors + locale = launchOptions.locale + port = launchOptions.port + preserveUserDataDir = launchOptions.preserveUserDataDir + proxy = launchOptions.proxy + userDataDir = launchOptions.userDataDir + viewport = launchOptions.viewport + additionalProperties = launchOptions.additionalProperties.toMutableMap() + } + + fun acceptDownloads(acceptDownloads: Boolean) = + acceptDownloads(JsonField.of(acceptDownloads)) + + /** + * Sets [Builder.acceptDownloads] to an arbitrary JSON value. + * + * You should usually call [Builder.acceptDownloads] with a well-typed [Boolean] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun acceptDownloads(acceptDownloads: JsonField) = apply { + this.acceptDownloads = acceptDownloads + } + + fun args(args: List) = args(JsonField.of(args)) + + /** + * Sets [Builder.args] to an arbitrary JSON value. + * + * You should usually call [Builder.args] with a well-typed `List` value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun args(args: JsonField>) = apply { + this.args = args.map { it.toMutableList() } + } + + /** + * Adds a single [String] to [args]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addArg(arg: String) = apply { + args = + (args ?: JsonField.of(mutableListOf())).also { + checkKnown("args", it).add(arg) + } + } + + fun cdpHeaders(cdpHeaders: CdpHeaders) = cdpHeaders(JsonField.of(cdpHeaders)) + + /** + * Sets [Builder.cdpHeaders] to an arbitrary JSON value. + * + * You should usually call [Builder.cdpHeaders] with a well-typed [CdpHeaders] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun cdpHeaders(cdpHeaders: JsonField) = apply { + this.cdpHeaders = cdpHeaders + } + + fun cdpUrl(cdpUrl: String) = cdpUrl(JsonField.of(cdpUrl)) + + /** + * Sets [Builder.cdpUrl] to an arbitrary JSON value. + * + * You should usually call [Builder.cdpUrl] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun cdpUrl(cdpUrl: JsonField) = apply { this.cdpUrl = cdpUrl } + + fun chromiumSandbox(chromiumSandbox: Boolean) = + chromiumSandbox(JsonField.of(chromiumSandbox)) + + /** + * Sets [Builder.chromiumSandbox] to an arbitrary JSON value. + * + * You should usually call [Builder.chromiumSandbox] with a well-typed [Boolean] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun chromiumSandbox(chromiumSandbox: JsonField) = apply { + this.chromiumSandbox = chromiumSandbox + } + + fun connectTimeoutMs(connectTimeoutMs: Double) = + connectTimeoutMs(JsonField.of(connectTimeoutMs)) + + /** + * Sets [Builder.connectTimeoutMs] to an arbitrary JSON value. + * + * You should usually call [Builder.connectTimeoutMs] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun connectTimeoutMs(connectTimeoutMs: JsonField) = apply { + this.connectTimeoutMs = connectTimeoutMs + } + + fun deviceScaleFactor(deviceScaleFactor: Double) = + deviceScaleFactor(JsonField.of(deviceScaleFactor)) + + /** + * Sets [Builder.deviceScaleFactor] to an arbitrary JSON value. + * + * You should usually call [Builder.deviceScaleFactor] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun deviceScaleFactor(deviceScaleFactor: JsonField) = apply { + this.deviceScaleFactor = deviceScaleFactor + } + + fun devtools(devtools: Boolean) = devtools(JsonField.of(devtools)) + + /** + * Sets [Builder.devtools] to an arbitrary JSON value. + * + * You should usually call [Builder.devtools] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun devtools(devtools: JsonField) = apply { this.devtools = devtools } + + fun downloadsPath(downloadsPath: String) = + downloadsPath(JsonField.of(downloadsPath)) + + /** + * Sets [Builder.downloadsPath] to an arbitrary JSON value. + * + * You should usually call [Builder.downloadsPath] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun downloadsPath(downloadsPath: JsonField) = apply { + this.downloadsPath = downloadsPath + } + + fun executablePath(executablePath: String) = + executablePath(JsonField.of(executablePath)) + + /** + * Sets [Builder.executablePath] to an arbitrary JSON value. + * + * You should usually call [Builder.executablePath] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun executablePath(executablePath: JsonField) = apply { + this.executablePath = executablePath + } + + fun hasTouch(hasTouch: Boolean) = hasTouch(JsonField.of(hasTouch)) + + /** + * Sets [Builder.hasTouch] to an arbitrary JSON value. + * + * You should usually call [Builder.hasTouch] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun hasTouch(hasTouch: JsonField) = apply { this.hasTouch = hasTouch } + + fun headless(headless: Boolean) = headless(JsonField.of(headless)) + + /** + * Sets [Builder.headless] to an arbitrary JSON value. + * + * You should usually call [Builder.headless] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun headless(headless: JsonField) = apply { this.headless = headless } + + fun ignoreDefaultArgs(ignoreDefaultArgs: IgnoreDefaultArgs) = + ignoreDefaultArgs(JsonField.of(ignoreDefaultArgs)) + + /** + * Sets [Builder.ignoreDefaultArgs] to an arbitrary JSON value. + * + * You should usually call [Builder.ignoreDefaultArgs] with a well-typed + * [IgnoreDefaultArgs] value instead. This method is primarily for setting the field + * to an undocumented or not yet supported value. + */ + fun ignoreDefaultArgs(ignoreDefaultArgs: JsonField) = apply { + this.ignoreDefaultArgs = ignoreDefaultArgs + } + + /** Alias for calling [ignoreDefaultArgs] with `IgnoreDefaultArgs.ofBool(bool)`. */ + fun ignoreDefaultArgs(bool: Boolean) = + ignoreDefaultArgs(IgnoreDefaultArgs.ofBool(bool)) + + /** + * Alias for calling [ignoreDefaultArgs] with + * `IgnoreDefaultArgs.ofStrings(strings)`. + */ + fun ignoreDefaultArgsOfStrings(strings: List) = + ignoreDefaultArgs(IgnoreDefaultArgs.ofStrings(strings)) + + fun ignoreHttpsErrors(ignoreHttpsErrors: Boolean) = + ignoreHttpsErrors(JsonField.of(ignoreHttpsErrors)) + + /** + * Sets [Builder.ignoreHttpsErrors] to an arbitrary JSON value. + * + * You should usually call [Builder.ignoreHttpsErrors] with a well-typed [Boolean] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun ignoreHttpsErrors(ignoreHttpsErrors: JsonField) = apply { + this.ignoreHttpsErrors = ignoreHttpsErrors + } + + fun locale(locale: String) = locale(JsonField.of(locale)) + + /** + * Sets [Builder.locale] to an arbitrary JSON value. + * + * You should usually call [Builder.locale] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun locale(locale: JsonField) = apply { this.locale = locale } + + fun port(port: Double) = port(JsonField.of(port)) + + /** + * Sets [Builder.port] to an arbitrary JSON value. + * + * You should usually call [Builder.port] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun port(port: JsonField) = apply { this.port = port } + + fun preserveUserDataDir(preserveUserDataDir: Boolean) = + preserveUserDataDir(JsonField.of(preserveUserDataDir)) + + /** + * Sets [Builder.preserveUserDataDir] to an arbitrary JSON value. + * + * You should usually call [Builder.preserveUserDataDir] with a well-typed [Boolean] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun preserveUserDataDir(preserveUserDataDir: JsonField) = apply { + this.preserveUserDataDir = preserveUserDataDir + } + + fun proxy(proxy: Proxy) = proxy(JsonField.of(proxy)) + + /** + * Sets [Builder.proxy] to an arbitrary JSON value. + * + * You should usually call [Builder.proxy] with a well-typed [Proxy] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun proxy(proxy: JsonField) = apply { this.proxy = proxy } + + fun userDataDir(userDataDir: String) = userDataDir(JsonField.of(userDataDir)) + + /** + * Sets [Builder.userDataDir] to an arbitrary JSON value. + * + * You should usually call [Builder.userDataDir] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun userDataDir(userDataDir: JsonField) = apply { + this.userDataDir = userDataDir + } + + fun viewport(viewport: Viewport) = viewport(JsonField.of(viewport)) + + /** + * Sets [Builder.viewport] to an arbitrary JSON value. + * + * You should usually call [Builder.viewport] with a well-typed [Viewport] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun viewport(viewport: JsonField) = apply { this.viewport = viewport } + + 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 [LaunchOptions]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): LaunchOptions = + LaunchOptions( + acceptDownloads, + (args ?: JsonMissing.of()).map { it.toImmutable() }, + cdpHeaders, + cdpUrl, + chromiumSandbox, + connectTimeoutMs, + deviceScaleFactor, + devtools, + downloadsPath, + executablePath, + hasTouch, + headless, + ignoreDefaultArgs, + ignoreHttpsErrors, + locale, + port, + preserveUserDataDir, + proxy, + userDataDir, + viewport, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): LaunchOptions = apply { + if (validated) { + return@apply + } + + acceptDownloads() + args() + cdpHeaders().ifPresent { it.validate() } + cdpUrl() + chromiumSandbox() + connectTimeoutMs() + deviceScaleFactor() + devtools() + downloadsPath() + executablePath() + hasTouch() + headless() + ignoreDefaultArgs().ifPresent { it.validate() } + ignoreHttpsErrors() + locale() + port() + preserveUserDataDir() + proxy().ifPresent { it.validate() } + userDataDir() + viewport().ifPresent { it.validate() } + 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 = + (if (acceptDownloads.asKnown().isPresent) 1 else 0) + + (args.asKnown().getOrNull()?.size ?: 0) + + (cdpHeaders.asKnown().getOrNull()?.validity() ?: 0) + + (if (cdpUrl.asKnown().isPresent) 1 else 0) + + (if (chromiumSandbox.asKnown().isPresent) 1 else 0) + + (if (connectTimeoutMs.asKnown().isPresent) 1 else 0) + + (if (deviceScaleFactor.asKnown().isPresent) 1 else 0) + + (if (devtools.asKnown().isPresent) 1 else 0) + + (if (downloadsPath.asKnown().isPresent) 1 else 0) + + (if (executablePath.asKnown().isPresent) 1 else 0) + + (if (hasTouch.asKnown().isPresent) 1 else 0) + + (if (headless.asKnown().isPresent) 1 else 0) + + (ignoreDefaultArgs.asKnown().getOrNull()?.validity() ?: 0) + + (if (ignoreHttpsErrors.asKnown().isPresent) 1 else 0) + + (if (locale.asKnown().isPresent) 1 else 0) + + (if (port.asKnown().isPresent) 1 else 0) + + (if (preserveUserDataDir.asKnown().isPresent) 1 else 0) + + (proxy.asKnown().getOrNull()?.validity() ?: 0) + + (if (userDataDir.asKnown().isPresent) 1 else 0) + + (viewport.asKnown().getOrNull()?.validity() ?: 0) + + class CdpHeaders + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [CdpHeaders]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [CdpHeaders]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(cdpHeaders: CdpHeaders) = apply { + additionalProperties = cdpHeaders.additionalProperties.toMutableMap() + } + + 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 [CdpHeaders]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): CdpHeaders = CdpHeaders(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): CdpHeaders = 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 = + additionalProperties.count { (_, value) -> + !value.isNull() && !value.isMissing() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is CdpHeaders && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "CdpHeaders{additionalProperties=$additionalProperties}" + } + + @JsonDeserialize(using = IgnoreDefaultArgs.Deserializer::class) + @JsonSerialize(using = IgnoreDefaultArgs.Serializer::class) + class IgnoreDefaultArgs + private constructor( + private val bool: Boolean? = null, + private val strings: List? = null, + private val _json: JsonValue? = null, + ) { + + fun bool(): Optional = Optional.ofNullable(bool) + + fun strings(): Optional> = Optional.ofNullable(strings) + + fun isBool(): Boolean = bool != null + + fun isStrings(): Boolean = strings != null + + fun asBool(): Boolean = bool.getOrThrow("bool") + + fun asStrings(): List = strings.getOrThrow("strings") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given + * [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the + * API, unless [visitor] overrides [Visitor.unknown]. To handle variants not known + * to this version of the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = ignoreDefaultArgs.accept(new IgnoreDefaultArgs.Visitor>() { + * @Override + * public Optional visitBool(Boolean bool) { + * return Optional.of(bool.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in + * [visitor] and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + bool != null -> visitor.visitBool(bool) + strings != null -> visitor.visitStrings(strings) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): IgnoreDefaultArgs = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitBool(bool: Boolean) {} + + override fun visitStrings(strings: List) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitBool(bool: Boolean) = 1 + + override fun visitStrings(strings: List) = strings.size + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is IgnoreDefaultArgs && + bool == other.bool && + strings == other.strings + } + + override fun hashCode(): Int = Objects.hash(bool, strings) + + override fun toString(): String = + when { + bool != null -> "IgnoreDefaultArgs{bool=$bool}" + strings != null -> "IgnoreDefaultArgs{strings=$strings}" + _json != null -> "IgnoreDefaultArgs{_unknown=$_json}" + else -> throw IllegalStateException("Invalid IgnoreDefaultArgs") + } + + companion object { + + @JvmStatic fun ofBool(bool: Boolean) = IgnoreDefaultArgs(bool = bool) + + @JvmStatic + fun ofStrings(strings: List) = + IgnoreDefaultArgs(strings = strings.toImmutable()) + } + + /** + * An interface that defines how to map each variant of [IgnoreDefaultArgs] to a + * value of type [T]. + */ + interface Visitor { + + fun visitBool(bool: Boolean): T + + fun visitStrings(strings: List): T + + /** + * Maps an unknown variant of [IgnoreDefaultArgs] to a value of type [T]. + * + * An instance of [IgnoreDefaultArgs] can contain an unknown variant if it was + * deserialized from data that doesn't match any known variant. For example, if + * the SDK is on an older version than the API, then the API may respond with + * new variants that the SDK is unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown IgnoreDefaultArgs: $json") + } + } + + internal class Deserializer : + BaseDeserializer(IgnoreDefaultArgs::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): IgnoreDefaultArgs { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + IgnoreDefaultArgs(bool = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef>())?.let { + IgnoreDefaultArgs(strings = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely + // incompatible with all the possible variants (e.g. deserializing from + // string). + 0 -> IgnoreDefaultArgs(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use + // the first completely valid match, or simply the first match if none + // are completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : + BaseSerializer(IgnoreDefaultArgs::class) { + + override fun serialize( + value: IgnoreDefaultArgs, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.bool != null -> generator.writeObject(value.bool) + value.strings != null -> generator.writeObject(value.strings) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid IgnoreDefaultArgs") + } + } + } + } + + class Proxy + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val server: JsonField, + private val bypass: JsonField, + private val password: JsonField, + private val username: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("server") + @ExcludeMissing + server: JsonField = JsonMissing.of(), + @JsonProperty("bypass") + @ExcludeMissing + bypass: JsonField = JsonMissing.of(), + @JsonProperty("password") + @ExcludeMissing + password: JsonField = JsonMissing.of(), + @JsonProperty("username") + @ExcludeMissing + username: JsonField = JsonMissing.of(), + ) : this(server, bypass, password, username, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun server(): String = server.getRequired("server") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun bypass(): Optional = bypass.getOptional("bypass") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun password(): Optional = password.getOptional("password") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun username(): Optional = username.getOptional("username") + + /** + * Returns the raw JSON value of [server]. + * + * Unlike [server], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("server") @ExcludeMissing fun _server(): JsonField = server + + /** + * Returns the raw JSON value of [bypass]. + * + * Unlike [bypass], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("bypass") @ExcludeMissing fun _bypass(): JsonField = bypass + + /** + * Returns the raw JSON value of [password]. + * + * Unlike [password], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("password") + @ExcludeMissing + fun _password(): JsonField = password + + /** + * Returns the raw JSON value of [username]. + * + * Unlike [username], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("username") + @ExcludeMissing + fun _username(): JsonField = username + + @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 [Proxy]. + * + * The following fields are required: + * ```java + * .server() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Proxy]. */ + class Builder internal constructor() { + + private var server: JsonField? = null + private var bypass: JsonField = JsonMissing.of() + private var password: JsonField = JsonMissing.of() + private var username: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(proxy: Proxy) = apply { + server = proxy.server + bypass = proxy.bypass + password = proxy.password + username = proxy.username + additionalProperties = proxy.additionalProperties.toMutableMap() + } + + fun server(server: String) = server(JsonField.of(server)) + + /** + * Sets [Builder.server] to an arbitrary JSON value. + * + * You should usually call [Builder.server] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun server(server: JsonField) = apply { this.server = server } + + fun bypass(bypass: String) = bypass(JsonField.of(bypass)) + + /** + * Sets [Builder.bypass] to an arbitrary JSON value. + * + * You should usually call [Builder.bypass] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun bypass(bypass: JsonField) = apply { this.bypass = bypass } + + fun password(password: String) = password(JsonField.of(password)) + + /** + * Sets [Builder.password] to an arbitrary JSON value. + * + * You should usually call [Builder.password] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun password(password: JsonField) = apply { this.password = password } + + fun username(username: String) = username(JsonField.of(username)) + + /** + * Sets [Builder.username] to an arbitrary JSON value. + * + * You should usually call [Builder.username] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun username(username: JsonField) = apply { this.username = username } + + 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 [Proxy]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .server() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Proxy = + Proxy( + checkRequired("server", server), + bypass, + password, + username, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Proxy = apply { + if (validated) { + return@apply + } + + server() + bypass() + password() + username() + 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 = + (if (server.asKnown().isPresent) 1 else 0) + + (if (bypass.asKnown().isPresent) 1 else 0) + + (if (password.asKnown().isPresent) 1 else 0) + + (if (username.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Proxy && + server == other.server && + bypass == other.bypass && + password == other.password && + username == other.username && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(server, bypass, password, username, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Proxy{server=$server, bypass=$bypass, password=$password, username=$username, additionalProperties=$additionalProperties}" + } + + class Viewport + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val height: JsonField, + private val width: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("height") + @ExcludeMissing + height: JsonField = JsonMissing.of(), + @JsonProperty("width") + @ExcludeMissing + width: JsonField = JsonMissing.of(), + ) : this(height, width, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun height(): Double = height.getRequired("height") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun width(): Double = width.getRequired("width") + + /** + * Returns the raw JSON value of [height]. + * + * Unlike [height], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("height") @ExcludeMissing fun _height(): JsonField = height + + /** + * Returns the raw JSON value of [width]. + * + * Unlike [width], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("width") @ExcludeMissing fun _width(): JsonField = width + + @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 [Viewport]. + * + * The following fields are required: + * ```java + * .height() + * .width() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Viewport]. */ + class Builder internal constructor() { + + private var height: JsonField? = null + private var width: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(viewport: Viewport) = apply { + height = viewport.height + width = viewport.width + additionalProperties = viewport.additionalProperties.toMutableMap() + } + + fun height(height: Double) = height(JsonField.of(height)) + + /** + * Sets [Builder.height] to an arbitrary JSON value. + * + * You should usually call [Builder.height] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun height(height: JsonField) = apply { this.height = height } + + fun width(width: Double) = width(JsonField.of(width)) + + /** + * Sets [Builder.width] to an arbitrary JSON value. + * + * You should usually call [Builder.width] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun width(width: JsonField) = apply { this.width = width } + + 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 [Viewport]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .height() + * .width() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Viewport = + Viewport( + checkRequired("height", height), + checkRequired("width", width), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Viewport = apply { + if (validated) { + return@apply + } + + height() + width() + 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 = + (if (height.asKnown().isPresent) 1 else 0) + + (if (width.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Viewport && + height == other.height && + width == other.width && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(height, width, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Viewport{height=$height, width=$width, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is LaunchOptions && + acceptDownloads == other.acceptDownloads && + args == other.args && + cdpHeaders == other.cdpHeaders && + cdpUrl == other.cdpUrl && + chromiumSandbox == other.chromiumSandbox && + connectTimeoutMs == other.connectTimeoutMs && + deviceScaleFactor == other.deviceScaleFactor && + devtools == other.devtools && + downloadsPath == other.downloadsPath && + executablePath == other.executablePath && + hasTouch == other.hasTouch && + headless == other.headless && + ignoreDefaultArgs == other.ignoreDefaultArgs && + ignoreHttpsErrors == other.ignoreHttpsErrors && + locale == other.locale && + port == other.port && + preserveUserDataDir == other.preserveUserDataDir && + proxy == other.proxy && + userDataDir == other.userDataDir && + viewport == other.viewport && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + acceptDownloads, + args, + cdpHeaders, + cdpUrl, + chromiumSandbox, + connectTimeoutMs, + deviceScaleFactor, + devtools, + downloadsPath, + executablePath, + hasTouch, + headless, + ignoreDefaultArgs, + ignoreHttpsErrors, + locale, + port, + preserveUserDataDir, + proxy, + userDataDir, + viewport, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "LaunchOptions{acceptDownloads=$acceptDownloads, args=$args, cdpHeaders=$cdpHeaders, cdpUrl=$cdpUrl, chromiumSandbox=$chromiumSandbox, connectTimeoutMs=$connectTimeoutMs, deviceScaleFactor=$deviceScaleFactor, devtools=$devtools, downloadsPath=$downloadsPath, executablePath=$executablePath, hasTouch=$hasTouch, headless=$headless, ignoreDefaultArgs=$ignoreDefaultArgs, ignoreHttpsErrors=$ignoreHttpsErrors, locale=$locale, port=$port, preserveUserDataDir=$preserveUserDataDir, proxy=$proxy, userDataDir=$userDataDir, viewport=$viewport, additionalProperties=$additionalProperties}" + } + + /** Browser type to use */ + class Type @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is + * on an older version than the API, then the API may respond with new members that the + * SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val LOCAL = of("local") + + @JvmField val BROWSERBASE = of("browserbase") + + @JvmStatic fun of(value: String) = Type(JsonField.of(value)) + } + + /** An enum containing [Type]'s known values. */ + enum class Known { + LOCAL, + BROWSERBASE, + } + + /** + * An enum containing [Type]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Type] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + LOCAL, + BROWSERBASE, + /** An enum member indicating that [Type] was instantiated with an unknown value. */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you + * want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + LOCAL -> Value.LOCAL + BROWSERBASE -> Value.BROWSERBASE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + LOCAL -> Known.LOCAL + BROWSERBASE -> Known.BROWSERBASE + else -> throw StagehandInvalidDataException("Unknown Type: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have + * the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Type = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Type && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Browser && + cdpUrl == other.cdpUrl && + launchOptions == other.launchOptions && + type == other.type && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(cdpUrl, launchOptions, type, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Browser{cdpUrl=$cdpUrl, launchOptions=$launchOptions, type=$type, additionalProperties=$additionalProperties}" + } + + class BrowserbaseSessionCreateParams + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val browserSettings: JsonField, + private val extensionId: JsonField, + private val keepAlive: JsonField, + private val projectId: JsonField, + private val proxies: JsonField, + private val region: JsonField, + private val timeout: JsonField, + private val userMetadata: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("browserSettings") + @ExcludeMissing + browserSettings: JsonField = JsonMissing.of(), + @JsonProperty("extensionId") + @ExcludeMissing + extensionId: JsonField = JsonMissing.of(), + @JsonProperty("keepAlive") + @ExcludeMissing + keepAlive: JsonField = JsonMissing.of(), + @JsonProperty("projectId") + @ExcludeMissing + projectId: JsonField = JsonMissing.of(), + @JsonProperty("proxies") @ExcludeMissing proxies: JsonField = JsonMissing.of(), + @JsonProperty("region") @ExcludeMissing region: JsonField = JsonMissing.of(), + @JsonProperty("timeout") @ExcludeMissing timeout: JsonField = JsonMissing.of(), + @JsonProperty("userMetadata") + @ExcludeMissing + userMetadata: JsonField = JsonMissing.of(), + ) : this( + browserSettings, + extensionId, + keepAlive, + projectId, + proxies, + region, + timeout, + userMetadata, + mutableMapOf(), + ) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun browserSettings(): Optional = + browserSettings.getOptional("browserSettings") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun extensionId(): Optional = extensionId.getOptional("extensionId") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun keepAlive(): Optional = keepAlive.getOptional("keepAlive") + + /** + * Deprecated. Browserbase API keys are now project-scoped, so this field is no longer + * required. + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + @Deprecated("deprecated") + fun projectId(): Optional = projectId.getOptional("projectId") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun proxies(): Optional = proxies.getOptional("proxies") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun region(): Optional = region.getOptional("region") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun timeout(): Optional = timeout.getOptional("timeout") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun userMetadata(): Optional = userMetadata.getOptional("userMetadata") + + /** + * Returns the raw JSON value of [browserSettings]. + * + * Unlike [browserSettings], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("browserSettings") + @ExcludeMissing + fun _browserSettings(): JsonField = browserSettings + + /** + * Returns the raw JSON value of [extensionId]. + * + * Unlike [extensionId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("extensionId") + @ExcludeMissing + fun _extensionId(): JsonField = extensionId + + /** + * Returns the raw JSON value of [keepAlive]. + * + * Unlike [keepAlive], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("keepAlive") @ExcludeMissing fun _keepAlive(): JsonField = keepAlive + + /** + * Returns the raw JSON value of [projectId]. + * + * Unlike [projectId], this method doesn't throw if the JSON field has an unexpected type. + */ + @Deprecated("deprecated") + @JsonProperty("projectId") + @ExcludeMissing + fun _projectId(): JsonField = projectId + + /** + * Returns the raw JSON value of [proxies]. + * + * Unlike [proxies], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("proxies") @ExcludeMissing fun _proxies(): JsonField = proxies + + /** + * Returns the raw JSON value of [region]. + * + * Unlike [region], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("region") @ExcludeMissing fun _region(): JsonField = region + + /** + * Returns the raw JSON value of [timeout]. + * + * Unlike [timeout], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("timeout") @ExcludeMissing fun _timeout(): JsonField = timeout + + /** + * Returns the raw JSON value of [userMetadata]. + * + * Unlike [userMetadata], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("userMetadata") + @ExcludeMissing + fun _userMetadata(): JsonField = userMetadata + + @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 + * [BrowserbaseSessionCreateParams]. + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [BrowserbaseSessionCreateParams]. */ + class Builder internal constructor() { + + private var browserSettings: JsonField = JsonMissing.of() + private var extensionId: JsonField = JsonMissing.of() + private var keepAlive: JsonField = JsonMissing.of() + private var projectId: JsonField = JsonMissing.of() + private var proxies: JsonField = JsonMissing.of() + private var region: JsonField = JsonMissing.of() + private var timeout: JsonField = JsonMissing.of() + private var userMetadata: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(browserbaseSessionCreateParams: BrowserbaseSessionCreateParams) = + apply { + browserSettings = browserbaseSessionCreateParams.browserSettings + extensionId = browserbaseSessionCreateParams.extensionId + keepAlive = browserbaseSessionCreateParams.keepAlive + projectId = browserbaseSessionCreateParams.projectId + proxies = browserbaseSessionCreateParams.proxies + region = browserbaseSessionCreateParams.region + timeout = browserbaseSessionCreateParams.timeout + userMetadata = browserbaseSessionCreateParams.userMetadata + additionalProperties = + browserbaseSessionCreateParams.additionalProperties.toMutableMap() + } + + fun browserSettings(browserSettings: BrowserSettings) = + browserSettings(JsonField.of(browserSettings)) + + /** + * Sets [Builder.browserSettings] to an arbitrary JSON value. + * + * You should usually call [Builder.browserSettings] with a well-typed [BrowserSettings] + * value instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun browserSettings(browserSettings: JsonField) = apply { + this.browserSettings = browserSettings + } + + fun extensionId(extensionId: String) = extensionId(JsonField.of(extensionId)) + + /** + * Sets [Builder.extensionId] to an arbitrary JSON value. + * + * You should usually call [Builder.extensionId] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun extensionId(extensionId: JsonField) = apply { + this.extensionId = extensionId + } + + fun keepAlive(keepAlive: Boolean) = keepAlive(JsonField.of(keepAlive)) + + /** + * Sets [Builder.keepAlive] to an arbitrary JSON value. + * + * You should usually call [Builder.keepAlive] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun keepAlive(keepAlive: JsonField) = apply { this.keepAlive = keepAlive } + + /** + * Deprecated. Browserbase API keys are now project-scoped, so this field is no longer + * required. + */ + @Deprecated("deprecated") + fun projectId(projectId: String) = projectId(JsonField.of(projectId)) + + /** + * Sets [Builder.projectId] to an arbitrary JSON value. + * + * You should usually call [Builder.projectId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + @Deprecated("deprecated") + fun projectId(projectId: JsonField) = apply { this.projectId = projectId } + + fun proxies(proxies: Proxies) = proxies(JsonField.of(proxies)) + + /** + * Sets [Builder.proxies] to an arbitrary JSON value. + * + * You should usually call [Builder.proxies] with a well-typed [Proxies] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun proxies(proxies: JsonField) = apply { this.proxies = proxies } + + /** Alias for calling [proxies] with `Proxies.ofBool(bool)`. */ + fun proxies(bool: Boolean) = proxies(Proxies.ofBool(bool)) + + /** Alias for calling [proxies] with `Proxies.ofProxyConfigList(proxyConfigList)`. */ + fun proxiesOfProxyConfigList(proxyConfigList: List) = + proxies(Proxies.ofProxyConfigList(proxyConfigList)) + + fun region(region: Region) = region(JsonField.of(region)) + + /** + * Sets [Builder.region] to an arbitrary JSON value. + * + * You should usually call [Builder.region] with a well-typed [Region] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun region(region: JsonField) = apply { this.region = region } + + fun timeout(timeout: Double) = timeout(JsonField.of(timeout)) + + /** + * Sets [Builder.timeout] to an arbitrary JSON value. + * + * You should usually call [Builder.timeout] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun timeout(timeout: JsonField) = apply { this.timeout = timeout } + + fun userMetadata(userMetadata: UserMetadata) = userMetadata(JsonField.of(userMetadata)) + + /** + * Sets [Builder.userMetadata] to an arbitrary JSON value. + * + * You should usually call [Builder.userMetadata] with a well-typed [UserMetadata] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun userMetadata(userMetadata: JsonField) = apply { + this.userMetadata = userMetadata + } + + 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 [BrowserbaseSessionCreateParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): BrowserbaseSessionCreateParams = + BrowserbaseSessionCreateParams( + browserSettings, + extensionId, + keepAlive, + projectId, + proxies, + region, + timeout, + userMetadata, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): BrowserbaseSessionCreateParams = apply { + if (validated) { + return@apply + } + + browserSettings().ifPresent { it.validate() } + extensionId() + keepAlive() + projectId() + proxies().ifPresent { it.validate() } + region().ifPresent { it.validate() } + timeout() + userMetadata().ifPresent { it.validate() } + 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 = + (browserSettings.asKnown().getOrNull()?.validity() ?: 0) + + (if (extensionId.asKnown().isPresent) 1 else 0) + + (if (keepAlive.asKnown().isPresent) 1 else 0) + + (if (projectId.asKnown().isPresent) 1 else 0) + + (proxies.asKnown().getOrNull()?.validity() ?: 0) + + (region.asKnown().getOrNull()?.validity() ?: 0) + + (if (timeout.asKnown().isPresent) 1 else 0) + + (userMetadata.asKnown().getOrNull()?.validity() ?: 0) + + class BrowserSettings + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val advancedStealth: JsonField, + private val blockAds: JsonField, + private val captchaImageSelector: JsonField, + private val captchaInputSelector: JsonField, + private val context: JsonField, + private val extensionId: JsonField, + private val fingerprint: JsonField, + private val logSession: JsonField, + private val os: JsonField, + private val recordSession: JsonField, + private val solveCaptchas: JsonField, + private val verified: JsonField, + private val viewport: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("advancedStealth") + @ExcludeMissing + advancedStealth: JsonField = JsonMissing.of(), + @JsonProperty("blockAds") + @ExcludeMissing + blockAds: JsonField = JsonMissing.of(), + @JsonProperty("captchaImageSelector") + @ExcludeMissing + captchaImageSelector: JsonField = JsonMissing.of(), + @JsonProperty("captchaInputSelector") + @ExcludeMissing + captchaInputSelector: JsonField = JsonMissing.of(), + @JsonProperty("context") + @ExcludeMissing + context: JsonField = JsonMissing.of(), + @JsonProperty("extensionId") + @ExcludeMissing + extensionId: JsonField = JsonMissing.of(), + @JsonProperty("fingerprint") + @ExcludeMissing + fingerprint: JsonField = JsonMissing.of(), + @JsonProperty("logSession") + @ExcludeMissing + logSession: JsonField = JsonMissing.of(), + @JsonProperty("os") @ExcludeMissing os: JsonField = JsonMissing.of(), + @JsonProperty("recordSession") + @ExcludeMissing + recordSession: JsonField = JsonMissing.of(), + @JsonProperty("solveCaptchas") + @ExcludeMissing + solveCaptchas: JsonField = JsonMissing.of(), + @JsonProperty("verified") + @ExcludeMissing + verified: JsonField = JsonMissing.of(), + @JsonProperty("viewport") + @ExcludeMissing + viewport: JsonField = JsonMissing.of(), + ) : this( + advancedStealth, + blockAds, + captchaImageSelector, + captchaInputSelector, + context, + extensionId, + fingerprint, + logSession, + os, + recordSession, + solveCaptchas, + verified, + viewport, + mutableMapOf(), + ) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun advancedStealth(): Optional = + advancedStealth.getOptional("advancedStealth") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun blockAds(): Optional = blockAds.getOptional("blockAds") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun captchaImageSelector(): Optional = + captchaImageSelector.getOptional("captchaImageSelector") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun captchaInputSelector(): Optional = + captchaInputSelector.getOptional("captchaInputSelector") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun context(): Optional = context.getOptional("context") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun extensionId(): Optional = extensionId.getOptional("extensionId") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun fingerprint(): Optional = fingerprint.getOptional("fingerprint") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun logSession(): Optional = logSession.getOptional("logSession") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun os(): Optional = os.getOptional("os") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun recordSession(): Optional = recordSession.getOptional("recordSession") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun solveCaptchas(): Optional = solveCaptchas.getOptional("solveCaptchas") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun verified(): Optional = verified.getOptional("verified") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun viewport(): Optional = viewport.getOptional("viewport") + + /** + * Returns the raw JSON value of [advancedStealth]. + * + * Unlike [advancedStealth], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("advancedStealth") + @ExcludeMissing + fun _advancedStealth(): JsonField = advancedStealth + + /** + * Returns the raw JSON value of [blockAds]. + * + * Unlike [blockAds], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("blockAds") @ExcludeMissing fun _blockAds(): JsonField = blockAds + + /** + * Returns the raw JSON value of [captchaImageSelector]. + * + * Unlike [captchaImageSelector], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("captchaImageSelector") + @ExcludeMissing + fun _captchaImageSelector(): JsonField = captchaImageSelector + + /** + * Returns the raw JSON value of [captchaInputSelector]. + * + * Unlike [captchaInputSelector], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("captchaInputSelector") + @ExcludeMissing + fun _captchaInputSelector(): JsonField = captchaInputSelector + + /** + * Returns the raw JSON value of [context]. + * + * Unlike [context], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("context") @ExcludeMissing fun _context(): JsonField = context + + /** + * Returns the raw JSON value of [extensionId]. + * + * Unlike [extensionId], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("extensionId") + @ExcludeMissing + fun _extensionId(): JsonField = extensionId + + /** + * Returns the raw JSON value of [fingerprint]. + * + * Unlike [fingerprint], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("fingerprint") + @ExcludeMissing + fun _fingerprint(): JsonField = fingerprint + + /** + * Returns the raw JSON value of [logSession]. + * + * Unlike [logSession], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("logSession") + @ExcludeMissing + fun _logSession(): JsonField = logSession + + /** + * Returns the raw JSON value of [os]. + * + * Unlike [os], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("os") @ExcludeMissing fun _os(): JsonField = os + + /** + * Returns the raw JSON value of [recordSession]. + * + * Unlike [recordSession], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("recordSession") + @ExcludeMissing + fun _recordSession(): JsonField = recordSession + + /** + * Returns the raw JSON value of [solveCaptchas]. + * + * Unlike [solveCaptchas], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("solveCaptchas") + @ExcludeMissing + fun _solveCaptchas(): JsonField = solveCaptchas + + /** + * Returns the raw JSON value of [verified]. + * + * Unlike [verified], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("verified") @ExcludeMissing fun _verified(): JsonField = verified + + /** + * Returns the raw JSON value of [viewport]. + * + * Unlike [viewport], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("viewport") + @ExcludeMissing + fun _viewport(): JsonField = viewport + + @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 [BrowserSettings]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [BrowserSettings]. */ + class Builder internal constructor() { + + private var advancedStealth: JsonField = JsonMissing.of() + private var blockAds: JsonField = JsonMissing.of() + private var captchaImageSelector: JsonField = JsonMissing.of() + private var captchaInputSelector: JsonField = JsonMissing.of() + private var context: JsonField = JsonMissing.of() + private var extensionId: JsonField = JsonMissing.of() + private var fingerprint: JsonField = JsonMissing.of() + private var logSession: JsonField = JsonMissing.of() + private var os: JsonField = JsonMissing.of() + private var recordSession: JsonField = JsonMissing.of() + private var solveCaptchas: JsonField = JsonMissing.of() + private var verified: JsonField = JsonMissing.of() + private var viewport: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(browserSettings: BrowserSettings) = apply { + advancedStealth = browserSettings.advancedStealth + blockAds = browserSettings.blockAds + captchaImageSelector = browserSettings.captchaImageSelector + captchaInputSelector = browserSettings.captchaInputSelector + context = browserSettings.context + extensionId = browserSettings.extensionId + fingerprint = browserSettings.fingerprint + logSession = browserSettings.logSession + os = browserSettings.os + recordSession = browserSettings.recordSession + solveCaptchas = browserSettings.solveCaptchas + verified = browserSettings.verified + viewport = browserSettings.viewport + additionalProperties = browserSettings.additionalProperties.toMutableMap() + } + + fun advancedStealth(advancedStealth: Boolean) = + advancedStealth(JsonField.of(advancedStealth)) + + /** + * Sets [Builder.advancedStealth] to an arbitrary JSON value. + * + * You should usually call [Builder.advancedStealth] with a well-typed [Boolean] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun advancedStealth(advancedStealth: JsonField) = apply { + this.advancedStealth = advancedStealth + } + + fun blockAds(blockAds: Boolean) = blockAds(JsonField.of(blockAds)) + + /** + * Sets [Builder.blockAds] to an arbitrary JSON value. + * + * You should usually call [Builder.blockAds] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun blockAds(blockAds: JsonField) = apply { this.blockAds = blockAds } + + fun captchaImageSelector(captchaImageSelector: String) = + captchaImageSelector(JsonField.of(captchaImageSelector)) + + /** + * Sets [Builder.captchaImageSelector] to an arbitrary JSON value. + * + * You should usually call [Builder.captchaImageSelector] with a well-typed [String] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun captchaImageSelector(captchaImageSelector: JsonField) = apply { + this.captchaImageSelector = captchaImageSelector + } + + fun captchaInputSelector(captchaInputSelector: String) = + captchaInputSelector(JsonField.of(captchaInputSelector)) + + /** + * Sets [Builder.captchaInputSelector] to an arbitrary JSON value. + * + * You should usually call [Builder.captchaInputSelector] with a well-typed [String] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun captchaInputSelector(captchaInputSelector: JsonField) = apply { + this.captchaInputSelector = captchaInputSelector + } + + fun context(context: Context) = context(JsonField.of(context)) + + /** + * Sets [Builder.context] to an arbitrary JSON value. + * + * You should usually call [Builder.context] with a well-typed [Context] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun context(context: JsonField) = apply { this.context = context } + + fun extensionId(extensionId: String) = extensionId(JsonField.of(extensionId)) + + /** + * Sets [Builder.extensionId] to an arbitrary JSON value. + * + * You should usually call [Builder.extensionId] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun extensionId(extensionId: JsonField) = apply { + this.extensionId = extensionId + } + + fun fingerprint(fingerprint: Fingerprint) = fingerprint(JsonField.of(fingerprint)) + + /** + * Sets [Builder.fingerprint] to an arbitrary JSON value. + * + * You should usually call [Builder.fingerprint] with a well-typed [Fingerprint] + * value instead. This method is primarily for setting the field to an undocumented + * or not yet supported value. + */ + fun fingerprint(fingerprint: JsonField) = apply { + this.fingerprint = fingerprint + } + + fun logSession(logSession: Boolean) = logSession(JsonField.of(logSession)) + + /** + * Sets [Builder.logSession] to an arbitrary JSON value. + * + * You should usually call [Builder.logSession] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun logSession(logSession: JsonField) = apply { + this.logSession = logSession + } + + fun os(os: Os) = os(JsonField.of(os)) + + /** + * Sets [Builder.os] to an arbitrary JSON value. + * + * You should usually call [Builder.os] with a well-typed [Os] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun os(os: JsonField) = apply { this.os = os } + + fun recordSession(recordSession: Boolean) = + recordSession(JsonField.of(recordSession)) + + /** + * Sets [Builder.recordSession] to an arbitrary JSON value. + * + * You should usually call [Builder.recordSession] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun recordSession(recordSession: JsonField) = apply { + this.recordSession = recordSession + } + + fun solveCaptchas(solveCaptchas: Boolean) = + solveCaptchas(JsonField.of(solveCaptchas)) + + /** + * Sets [Builder.solveCaptchas] to an arbitrary JSON value. + * + * You should usually call [Builder.solveCaptchas] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun solveCaptchas(solveCaptchas: JsonField) = apply { + this.solveCaptchas = solveCaptchas + } + + fun verified(verified: Boolean) = verified(JsonField.of(verified)) + + /** + * Sets [Builder.verified] to an arbitrary JSON value. + * + * You should usually call [Builder.verified] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun verified(verified: JsonField) = apply { this.verified = verified } + + fun viewport(viewport: Viewport) = viewport(JsonField.of(viewport)) + + /** + * Sets [Builder.viewport] to an arbitrary JSON value. + * + * You should usually call [Builder.viewport] with a well-typed [Viewport] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun viewport(viewport: JsonField) = apply { this.viewport = viewport } + + 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 [BrowserSettings]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): BrowserSettings = + BrowserSettings( + advancedStealth, + blockAds, + captchaImageSelector, + captchaInputSelector, + context, + extensionId, + fingerprint, + logSession, + os, + recordSession, + solveCaptchas, + verified, + viewport, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): BrowserSettings = apply { + if (validated) { + return@apply + } + + advancedStealth() + blockAds() + captchaImageSelector() + captchaInputSelector() + context().ifPresent { it.validate() } + extensionId() + fingerprint().ifPresent { it.validate() } + logSession() + os().ifPresent { it.validate() } + recordSession() + solveCaptchas() + verified() + viewport().ifPresent { it.validate() } + 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 = + (if (advancedStealth.asKnown().isPresent) 1 else 0) + + (if (blockAds.asKnown().isPresent) 1 else 0) + + (if (captchaImageSelector.asKnown().isPresent) 1 else 0) + + (if (captchaInputSelector.asKnown().isPresent) 1 else 0) + + (context.asKnown().getOrNull()?.validity() ?: 0) + + (if (extensionId.asKnown().isPresent) 1 else 0) + + (fingerprint.asKnown().getOrNull()?.validity() ?: 0) + + (if (logSession.asKnown().isPresent) 1 else 0) + + (os.asKnown().getOrNull()?.validity() ?: 0) + + (if (recordSession.asKnown().isPresent) 1 else 0) + + (if (solveCaptchas.asKnown().isPresent) 1 else 0) + + (if (verified.asKnown().isPresent) 1 else 0) + + (viewport.asKnown().getOrNull()?.validity() ?: 0) + + class Context + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val id: JsonField, + private val persist: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("id") @ExcludeMissing id: JsonField = JsonMissing.of(), + @JsonProperty("persist") + @ExcludeMissing + persist: JsonField = JsonMissing.of(), + ) : this(id, persist, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or + * is unexpectedly missing or null (e.g. if the server responded with an + * unexpected value). + */ + fun id(): String = id.getRequired("id") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun persist(): Optional = persist.getOptional("persist") + + /** + * Returns the raw JSON value of [id]. + * + * Unlike [id], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("id") @ExcludeMissing fun _id(): JsonField = id + + /** + * Returns the raw JSON value of [persist]. + * + * Unlike [persist], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("persist") + @ExcludeMissing + fun _persist(): JsonField = persist + + @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 [Context]. + * + * The following fields are required: + * ```java + * .id() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Context]. */ + class Builder internal constructor() { + + private var id: JsonField? = null + private var persist: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(context: Context) = apply { + id = context.id + persist = context.persist + additionalProperties = context.additionalProperties.toMutableMap() + } + + fun id(id: String) = id(JsonField.of(id)) + + /** + * Sets [Builder.id] to an arbitrary JSON value. + * + * You should usually call [Builder.id] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun id(id: JsonField) = apply { this.id = id } + + fun persist(persist: Boolean) = persist(JsonField.of(persist)) + + /** + * Sets [Builder.persist] to an arbitrary JSON value. + * + * You should usually call [Builder.persist] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun persist(persist: JsonField) = apply { this.persist = persist } + + 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 [Context]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .id() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Context = + Context( + checkRequired("id", id), + persist, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Context = apply { + if (validated) { + return@apply + } + + id() + persist() + 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 = + (if (id.asKnown().isPresent) 1 else 0) + + (if (persist.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Context && + id == other.id && + persist == other.persist && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(id, persist, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Context{id=$id, persist=$persist, additionalProperties=$additionalProperties}" + } + + class Fingerprint + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val browsers: JsonField>, + private val devices: JsonField>, + private val httpVersion: JsonField, + private val locales: JsonField>, + private val operatingSystems: JsonField>, + private val screen: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("browsers") + @ExcludeMissing + browsers: JsonField> = JsonMissing.of(), + @JsonProperty("devices") + @ExcludeMissing + devices: JsonField> = JsonMissing.of(), + @JsonProperty("httpVersion") + @ExcludeMissing + httpVersion: JsonField = JsonMissing.of(), + @JsonProperty("locales") + @ExcludeMissing + locales: JsonField> = JsonMissing.of(), + @JsonProperty("operatingSystems") + @ExcludeMissing + operatingSystems: JsonField> = JsonMissing.of(), + @JsonProperty("screen") + @ExcludeMissing + screen: JsonField = JsonMissing.of(), + ) : this( + browsers, + devices, + httpVersion, + locales, + operatingSystems, + screen, + mutableMapOf(), + ) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun browsers(): Optional> = browsers.getOptional("browsers") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun devices(): Optional> = devices.getOptional("devices") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun httpVersion(): Optional = httpVersion.getOptional("httpVersion") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun locales(): Optional> = locales.getOptional("locales") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun operatingSystems(): Optional> = + operatingSystems.getOptional("operatingSystems") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun screen(): Optional = screen.getOptional("screen") + + /** + * Returns the raw JSON value of [browsers]. + * + * Unlike [browsers], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("browsers") + @ExcludeMissing + fun _browsers(): JsonField> = browsers + + /** + * Returns the raw JSON value of [devices]. + * + * Unlike [devices], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("devices") + @ExcludeMissing + fun _devices(): JsonField> = devices + + /** + * Returns the raw JSON value of [httpVersion]. + * + * Unlike [httpVersion], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("httpVersion") + @ExcludeMissing + fun _httpVersion(): JsonField = httpVersion + + /** + * Returns the raw JSON value of [locales]. + * + * Unlike [locales], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("locales") + @ExcludeMissing + fun _locales(): JsonField> = locales + + /** + * Returns the raw JSON value of [operatingSystems]. + * + * Unlike [operatingSystems], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("operatingSystems") + @ExcludeMissing + fun _operatingSystems(): JsonField> = operatingSystems + + /** + * Returns the raw JSON value of [screen]. + * + * Unlike [screen], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("screen") @ExcludeMissing fun _screen(): JsonField = screen + + @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 [Fingerprint]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Fingerprint]. */ + class Builder internal constructor() { + + private var browsers: JsonField>? = null + private var devices: JsonField>? = null + private var httpVersion: JsonField = JsonMissing.of() + private var locales: JsonField>? = null + private var operatingSystems: JsonField>? = null + private var screen: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(fingerprint: Fingerprint) = apply { + browsers = fingerprint.browsers.map { it.toMutableList() } + devices = fingerprint.devices.map { it.toMutableList() } + httpVersion = fingerprint.httpVersion + locales = fingerprint.locales.map { it.toMutableList() } + operatingSystems = fingerprint.operatingSystems.map { it.toMutableList() } + screen = fingerprint.screen + additionalProperties = fingerprint.additionalProperties.toMutableMap() + } + + fun browsers(browsers: List) = browsers(JsonField.of(browsers)) + + /** + * Sets [Builder.browsers] to an arbitrary JSON value. + * + * You should usually call [Builder.browsers] with a well-typed `List` + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun browsers(browsers: JsonField>) = apply { + this.browsers = browsers.map { it.toMutableList() } + } + + /** + * Adds a single [Browser] to [browsers]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addBrowser(browser: Browser) = apply { + browsers = + (browsers ?: JsonField.of(mutableListOf())).also { + checkKnown("browsers", it).add(browser) + } + } + + fun devices(devices: List) = devices(JsonField.of(devices)) + + /** + * Sets [Builder.devices] to an arbitrary JSON value. + * + * You should usually call [Builder.devices] with a well-typed `List` + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun devices(devices: JsonField>) = apply { + this.devices = devices.map { it.toMutableList() } + } + + /** + * Adds a single [Device] to [devices]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addDevice(device: Device) = apply { + devices = + (devices ?: JsonField.of(mutableListOf())).also { + checkKnown("devices", it).add(device) + } + } + + fun httpVersion(httpVersion: HttpVersion) = + httpVersion(JsonField.of(httpVersion)) + + /** + * Sets [Builder.httpVersion] to an arbitrary JSON value. + * + * You should usually call [Builder.httpVersion] with a well-typed [HttpVersion] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun httpVersion(httpVersion: JsonField) = apply { + this.httpVersion = httpVersion + } + + fun locales(locales: List) = locales(JsonField.of(locales)) + + /** + * Sets [Builder.locales] to an arbitrary JSON value. + * + * You should usually call [Builder.locales] with a well-typed `List` + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun locales(locales: JsonField>) = apply { + this.locales = locales.map { it.toMutableList() } + } + + /** + * Adds a single [String] to [locales]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addLocale(locale: String) = apply { + locales = + (locales ?: JsonField.of(mutableListOf())).also { + checkKnown("locales", it).add(locale) + } + } + + fun operatingSystems(operatingSystems: List) = + operatingSystems(JsonField.of(operatingSystems)) + + /** + * Sets [Builder.operatingSystems] to an arbitrary JSON value. + * + * You should usually call [Builder.operatingSystems] with a well-typed + * `List` value instead. This method is primarily for setting + * the field to an undocumented or not yet supported value. + */ + fun operatingSystems(operatingSystems: JsonField>) = + apply { + this.operatingSystems = operatingSystems.map { it.toMutableList() } + } + + /** + * Adds a single [OperatingSystem] to [operatingSystems]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addOperatingSystem(operatingSystem: OperatingSystem) = apply { + operatingSystems = + (operatingSystems ?: JsonField.of(mutableListOf())).also { + checkKnown("operatingSystems", it).add(operatingSystem) + } + } + + fun screen(screen: Screen) = screen(JsonField.of(screen)) + + /** + * Sets [Builder.screen] to an arbitrary JSON value. + * + * You should usually call [Builder.screen] with a well-typed [Screen] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun screen(screen: JsonField) = apply { this.screen = screen } + + 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 [Fingerprint]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Fingerprint = + Fingerprint( + (browsers ?: JsonMissing.of()).map { it.toImmutable() }, + (devices ?: JsonMissing.of()).map { it.toImmutable() }, + httpVersion, + (locales ?: JsonMissing.of()).map { it.toImmutable() }, + (operatingSystems ?: JsonMissing.of()).map { it.toImmutable() }, + screen, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Fingerprint = apply { + if (validated) { + return@apply + } + + browsers().ifPresent { it.forEach { it.validate() } } + devices().ifPresent { it.forEach { it.validate() } } + httpVersion().ifPresent { it.validate() } + locales() + operatingSystems().ifPresent { it.forEach { it.validate() } } + screen().ifPresent { it.validate() } + 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 = + (browsers.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + (devices.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + (httpVersion.asKnown().getOrNull()?.validity() ?: 0) + + (locales.asKnown().getOrNull()?.size ?: 0) + + (operatingSystems.asKnown().getOrNull()?.sumOf { it.validity().toInt() } + ?: 0) + + (screen.asKnown().getOrNull()?.validity() ?: 0) + + class Browser + @JsonCreator + private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that + * doesn't match any known member, and you want to know that value. For example, + * if the SDK is on an older version than the API, then the API may respond with + * new members that the SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue + fun _value(): JsonField = value + + companion object { + + @JvmField val CHROME = of("chrome") + + @JvmField val EDGE = of("edge") + + @JvmField val FIREFOX = of("firefox") + + @JvmField val SAFARI = of("safari") + + @JvmStatic fun of(value: String) = Browser(JsonField.of(value)) + } + + /** An enum containing [Browser]'s known values. */ + enum class Known { + CHROME, + EDGE, + FIREFOX, + SAFARI, + } + + /** + * An enum containing [Browser]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Browser] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For + * example, if the SDK is on an older version than the API, then the API may + * respond with new members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + CHROME, + EDGE, + FIREFOX, + SAFARI, + /** + * An enum member indicating that [Browser] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or + * if you want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + CHROME -> Value.CHROME + EDGE -> Value.EDGE + FIREFOX -> Value.FIREFOX + SAFARI -> Value.SAFARI + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known + * and don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not + * a known member. + */ + fun known(): Known = + when (this) { + CHROME -> Known.CHROME + EDGE -> Known.EDGE + FIREFOX -> Known.FIREFOX + SAFARI -> Known.SAFARI + else -> throw StagehandInvalidDataException("Unknown Browser: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not + * have the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): Browser = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Browser && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + class Device + @JsonCreator + private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that + * doesn't match any known member, and you want to know that value. For example, + * if the SDK is on an older version than the API, then the API may respond with + * new members that the SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue + fun _value(): JsonField = value + + companion object { + + @JvmField val DESKTOP = of("desktop") + + @JvmField val MOBILE = of("mobile") + + @JvmStatic fun of(value: String) = Device(JsonField.of(value)) + } + + /** An enum containing [Device]'s known values. */ + enum class Known { + DESKTOP, + MOBILE, + } + + /** + * An enum containing [Device]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Device] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For + * example, if the SDK is on an older version than the API, then the API may + * respond with new members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + DESKTOP, + MOBILE, + /** + * An enum member indicating that [Device] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or + * if you want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + DESKTOP -> Value.DESKTOP + MOBILE -> Value.MOBILE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known + * and don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not + * a known member. + */ + fun known(): Known = + when (this) { + DESKTOP -> Known.DESKTOP + MOBILE -> Known.MOBILE + else -> throw StagehandInvalidDataException("Unknown Device: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not + * have the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): Device = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Device && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + class HttpVersion + @JsonCreator + private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that + * doesn't match any known member, and you want to know that value. For example, + * if the SDK is on an older version than the API, then the API may respond with + * new members that the SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue + fun _value(): JsonField = value + + companion object { + + @JvmField val _1 = of("1") + + @JvmField val _2 = of("2") + + @JvmStatic fun of(value: String) = HttpVersion(JsonField.of(value)) + } + + /** An enum containing [HttpVersion]'s known values. */ + enum class Known { + _1, + _2, + } + + /** + * An enum containing [HttpVersion]'s known values, as well as an [_UNKNOWN] + * member. + * + * An instance of [HttpVersion] can contain an unknown value in a couple of + * cases: + * - It was deserialized from data that doesn't match any known member. For + * example, if the SDK is on an older version than the API, then the API may + * respond with new members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + _1, + _2, + /** + * An enum member indicating that [HttpVersion] was instantiated with an + * unknown value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or + * if you want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + _1 -> Value._1 + _2 -> Value._2 + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known + * and don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not + * a known member. + */ + fun known(): Known = + when (this) { + _1 -> Known._1 + _2 -> Known._2 + else -> + throw StagehandInvalidDataException("Unknown HttpVersion: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not + * have the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): HttpVersion = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is HttpVersion && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + class OperatingSystem + @JsonCreator + private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that + * doesn't match any known member, and you want to know that value. For example, + * if the SDK is on an older version than the API, then the API may respond with + * new members that the SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue + fun _value(): JsonField = value + + companion object { + + @JvmField val ANDROID = of("android") + + @JvmField val IOS = of("ios") + + @JvmField val LINUX = of("linux") + + @JvmField val MACOS = of("macos") + + @JvmField val WINDOWS = of("windows") + + @JvmStatic fun of(value: String) = OperatingSystem(JsonField.of(value)) + } + + /** An enum containing [OperatingSystem]'s known values. */ + enum class Known { + ANDROID, + IOS, + LINUX, + MACOS, + WINDOWS, + } + + /** + * An enum containing [OperatingSystem]'s known values, as well as an [_UNKNOWN] + * member. + * + * An instance of [OperatingSystem] can contain an unknown value in a couple of + * cases: + * - It was deserialized from data that doesn't match any known member. For + * example, if the SDK is on an older version than the API, then the API may + * respond with new members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + ANDROID, + IOS, + LINUX, + MACOS, + WINDOWS, + /** + * An enum member indicating that [OperatingSystem] was instantiated with an + * unknown value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or + * if you want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + ANDROID -> Value.ANDROID + IOS -> Value.IOS + LINUX -> Value.LINUX + MACOS -> Value.MACOS + WINDOWS -> Value.WINDOWS + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known + * and don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not + * a known member. + */ + fun known(): Known = + when (this) { + ANDROID -> Known.ANDROID + IOS -> Known.IOS + LINUX -> Known.LINUX + MACOS -> Known.MACOS + WINDOWS -> Known.WINDOWS + else -> + throw StagehandInvalidDataException( + "Unknown OperatingSystem: $value" + ) + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not + * have the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): OperatingSystem = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is OperatingSystem && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + class Screen + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val maxHeight: JsonField, + private val maxWidth: JsonField, + private val minHeight: JsonField, + private val minWidth: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("maxHeight") + @ExcludeMissing + maxHeight: JsonField = JsonMissing.of(), + @JsonProperty("maxWidth") + @ExcludeMissing + maxWidth: JsonField = JsonMissing.of(), + @JsonProperty("minHeight") + @ExcludeMissing + minHeight: JsonField = JsonMissing.of(), + @JsonProperty("minWidth") + @ExcludeMissing + minWidth: JsonField = JsonMissing.of(), + ) : this(maxHeight, maxWidth, minHeight, minWidth, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun maxHeight(): Optional = maxHeight.getOptional("maxHeight") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun maxWidth(): Optional = maxWidth.getOptional("maxWidth") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun minHeight(): Optional = minHeight.getOptional("minHeight") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun minWidth(): Optional = minWidth.getOptional("minWidth") + + /** + * Returns the raw JSON value of [maxHeight]. + * + * Unlike [maxHeight], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("maxHeight") + @ExcludeMissing + fun _maxHeight(): JsonField = maxHeight + + /** + * Returns the raw JSON value of [maxWidth]. + * + * Unlike [maxWidth], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("maxWidth") + @ExcludeMissing + fun _maxWidth(): JsonField = maxWidth + + /** + * Returns the raw JSON value of [minHeight]. + * + * Unlike [minHeight], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("minHeight") + @ExcludeMissing + fun _minHeight(): JsonField = minHeight + + /** + * Returns the raw JSON value of [minWidth]. + * + * Unlike [minWidth], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("minWidth") + @ExcludeMissing + fun _minWidth(): JsonField = minWidth + + @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 [Screen]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Screen]. */ + class Builder internal constructor() { + + private var maxHeight: JsonField = JsonMissing.of() + private var maxWidth: JsonField = JsonMissing.of() + private var minHeight: JsonField = JsonMissing.of() + private var minWidth: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = + mutableMapOf() + + @JvmSynthetic + internal fun from(screen: Screen) = apply { + maxHeight = screen.maxHeight + maxWidth = screen.maxWidth + minHeight = screen.minHeight + minWidth = screen.minWidth + additionalProperties = screen.additionalProperties.toMutableMap() + } + + fun maxHeight(maxHeight: Double) = maxHeight(JsonField.of(maxHeight)) + + /** + * Sets [Builder.maxHeight] to an arbitrary JSON value. + * + * You should usually call [Builder.maxHeight] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun maxHeight(maxHeight: JsonField) = apply { + this.maxHeight = maxHeight + } + + fun maxWidth(maxWidth: Double) = maxWidth(JsonField.of(maxWidth)) + + /** + * Sets [Builder.maxWidth] to an arbitrary JSON value. + * + * You should usually call [Builder.maxWidth] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun maxWidth(maxWidth: JsonField) = apply { + this.maxWidth = maxWidth + } + + fun minHeight(minHeight: Double) = minHeight(JsonField.of(minHeight)) + + /** + * Sets [Builder.minHeight] to an arbitrary JSON value. + * + * You should usually call [Builder.minHeight] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun minHeight(minHeight: JsonField) = apply { + this.minHeight = minHeight + } + + fun minWidth(minWidth: Double) = minWidth(JsonField.of(minWidth)) + + /** + * Sets [Builder.minWidth] to an arbitrary JSON value. + * + * You should usually call [Builder.minWidth] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun minWidth(minWidth: JsonField) = apply { + this.minWidth = minWidth + } + + 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 [Screen]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Screen = + Screen( + maxHeight, + maxWidth, + minHeight, + minWidth, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): Screen = apply { + if (validated) { + return@apply + } + + maxHeight() + maxWidth() + minHeight() + minWidth() + 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 = + (if (maxHeight.asKnown().isPresent) 1 else 0) + + (if (maxWidth.asKnown().isPresent) 1 else 0) + + (if (minHeight.asKnown().isPresent) 1 else 0) + + (if (minWidth.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Screen && + maxHeight == other.maxHeight && + maxWidth == other.maxWidth && + minHeight == other.minHeight && + minWidth == other.minWidth && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(maxHeight, maxWidth, minHeight, minWidth, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Screen{maxHeight=$maxHeight, maxWidth=$maxWidth, minHeight=$minHeight, minWidth=$minWidth, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Fingerprint && + browsers == other.browsers && + devices == other.devices && + httpVersion == other.httpVersion && + locales == other.locales && + operatingSystems == other.operatingSystems && + screen == other.screen && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + browsers, + devices, + httpVersion, + locales, + operatingSystems, + screen, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Fingerprint{browsers=$browsers, devices=$devices, httpVersion=$httpVersion, locales=$locales, operatingSystems=$operatingSystems, screen=$screen, additionalProperties=$additionalProperties}" + } + + class Os @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that + * doesn't match any known member, and you want to know that value. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val WINDOWS = of("windows") + + @JvmField val MAC = of("mac") + + @JvmField val LINUX = of("linux") + + @JvmField val MOBILE = of("mobile") + + @JvmField val TABLET = of("tablet") + + @JvmStatic fun of(value: String) = Os(JsonField.of(value)) + } + + /** An enum containing [Os]'s known values. */ + enum class Known { + WINDOWS, + MAC, + LINUX, + MOBILE, + TABLET, + } + + /** + * An enum containing [Os]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Os] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, + * if the SDK is on an older version than the API, then the API may respond with + * new members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + WINDOWS, + MAC, + LINUX, + MOBILE, + TABLET, + /** + * An enum member indicating that [Os] was instantiated with an unknown value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if + * you want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + WINDOWS -> Value.WINDOWS + MAC -> Value.MAC + LINUX -> Value.LINUX + MOBILE -> Value.MOBILE + TABLET -> Value.TABLET + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a + * known member. + */ + fun known(): Known = + when (this) { + WINDOWS -> Known.WINDOWS + MAC -> Known.MAC + LINUX -> Known.LINUX + MOBILE -> Known.MOBILE + TABLET -> Known.TABLET + else -> throw StagehandInvalidDataException("Unknown Os: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not + * have the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Os = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Os && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + class Viewport + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val height: JsonField, + private val width: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("height") + @ExcludeMissing + height: JsonField = JsonMissing.of(), + @JsonProperty("width") + @ExcludeMissing + width: JsonField = JsonMissing.of(), + ) : this(height, width, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun height(): Optional = height.getOptional("height") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun width(): Optional = width.getOptional("width") + + /** + * Returns the raw JSON value of [height]. + * + * Unlike [height], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("height") @ExcludeMissing fun _height(): JsonField = height + + /** + * Returns the raw JSON value of [width]. + * + * Unlike [width], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("width") @ExcludeMissing fun _width(): JsonField = width + + @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 [Viewport]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Viewport]. */ + class Builder internal constructor() { + + private var height: JsonField = JsonMissing.of() + private var width: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(viewport: Viewport) = apply { + height = viewport.height + width = viewport.width + additionalProperties = viewport.additionalProperties.toMutableMap() + } + + fun height(height: Double) = height(JsonField.of(height)) + + /** + * Sets [Builder.height] to an arbitrary JSON value. + * + * You should usually call [Builder.height] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun height(height: JsonField) = apply { this.height = height } + + fun width(width: Double) = width(JsonField.of(width)) + + /** + * Sets [Builder.width] to an arbitrary JSON value. + * + * You should usually call [Builder.width] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun width(width: JsonField) = apply { this.width = width } + + 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 [Viewport]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Viewport = + Viewport(height, width, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Viewport = apply { + if (validated) { + return@apply + } + + height() + width() + 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 = + (if (height.asKnown().isPresent) 1 else 0) + + (if (width.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Viewport && + height == other.height && + width == other.width && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(height, width, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Viewport{height=$height, width=$width, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is BrowserSettings && + advancedStealth == other.advancedStealth && + blockAds == other.blockAds && + captchaImageSelector == other.captchaImageSelector && + captchaInputSelector == other.captchaInputSelector && + context == other.context && + extensionId == other.extensionId && + fingerprint == other.fingerprint && + logSession == other.logSession && + os == other.os && + recordSession == other.recordSession && + solveCaptchas == other.solveCaptchas && + verified == other.verified && + viewport == other.viewport && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + advancedStealth, + blockAds, + captchaImageSelector, + captchaInputSelector, + context, + extensionId, + fingerprint, + logSession, + os, + recordSession, + solveCaptchas, + verified, + viewport, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "BrowserSettings{advancedStealth=$advancedStealth, blockAds=$blockAds, captchaImageSelector=$captchaImageSelector, captchaInputSelector=$captchaInputSelector, context=$context, extensionId=$extensionId, fingerprint=$fingerprint, logSession=$logSession, os=$os, recordSession=$recordSession, solveCaptchas=$solveCaptchas, verified=$verified, viewport=$viewport, additionalProperties=$additionalProperties}" + } + + @JsonDeserialize(using = Proxies.Deserializer::class) + @JsonSerialize(using = Proxies.Serializer::class) + class Proxies + private constructor( + private val bool: Boolean? = null, + private val proxyConfigList: List? = null, + private val _json: JsonValue? = null, + ) { + + fun bool(): Optional = Optional.ofNullable(bool) + + fun proxyConfigList(): Optional> = + Optional.ofNullable(proxyConfigList) + + fun isBool(): Boolean = bool != null + + fun isProxyConfigList(): Boolean = proxyConfigList != null + + fun asBool(): Boolean = bool.getOrThrow("bool") + + fun asProxyConfigList(): List = + proxyConfigList.getOrThrow("proxyConfigList") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given + * [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, + * unless [visitor] overrides [Visitor.unknown]. To handle variants not known to this + * version of the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = proxies.accept(new Proxies.Visitor>() { + * @Override + * public Optional visitBool(Boolean bool) { + * return Optional.of(bool.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in + * [visitor] and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + bool != null -> visitor.visitBool(bool) + proxyConfigList != null -> visitor.visitProxyConfigList(proxyConfigList) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Proxies = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitBool(bool: Boolean) {} + + override fun visitProxyConfigList(proxyConfigList: List) { + proxyConfigList.forEach { it.validate() } + } + } + ) + 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 = + accept( + object : Visitor { + override fun visitBool(bool: Boolean) = 1 + + override fun visitProxyConfigList(proxyConfigList: List) = + proxyConfigList.sumOf { it.validity().toInt() } + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Proxies && + bool == other.bool && + proxyConfigList == other.proxyConfigList + } + + override fun hashCode(): Int = Objects.hash(bool, proxyConfigList) + + override fun toString(): String = + when { + bool != null -> "Proxies{bool=$bool}" + proxyConfigList != null -> "Proxies{proxyConfigList=$proxyConfigList}" + _json != null -> "Proxies{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Proxies") + } + + companion object { + + @JvmStatic fun ofBool(bool: Boolean) = Proxies(bool = bool) + + @JvmStatic + fun ofProxyConfigList(proxyConfigList: List) = + Proxies(proxyConfigList = proxyConfigList.toImmutable()) + } + + /** + * An interface that defines how to map each variant of [Proxies] to a value of type + * [T]. + */ + interface Visitor { + + fun visitBool(bool: Boolean): T + + fun visitProxyConfigList(proxyConfigList: List): T + + /** + * Maps an unknown variant of [Proxies] to a value of type [T]. + * + * An instance of [Proxies] can contain an unknown variant if it was deserialized + * from data that doesn't match any known variant. For example, if the SDK is on an + * older version than the API, then the API may respond with new variants that the + * SDK is unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Proxies: $json") + } + } + + internal class Deserializer : BaseDeserializer(Proxies::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Proxies { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Proxies(bool = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef>())?.let { + Proxies(proxyConfigList = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from string). + 0 -> Proxies(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Proxies::class) { + + override fun serialize( + value: Proxies, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.bool != null -> generator.writeObject(value.bool) + value.proxyConfigList != null -> + generator.writeObject(value.proxyConfigList) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Proxies") + } + } + } + + @JsonDeserialize(using = ProxyConfig.Deserializer::class) + @JsonSerialize(using = ProxyConfig.Serializer::class) + class ProxyConfig + private constructor( + private val browserbase: Browserbase? = null, + private val external: External? = null, + private val _json: JsonValue? = null, + ) { + + fun browserbase(): Optional = Optional.ofNullable(browserbase) + + fun external(): Optional = Optional.ofNullable(external) + + fun isBrowserbase(): Boolean = browserbase != null + + fun isExternal(): Boolean = external != null + + fun asBrowserbase(): Browserbase = browserbase.getOrThrow("browserbase") + + fun asExternal(): External = external.getOrThrow("external") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given + * [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the + * API, unless [visitor] overrides [Visitor.unknown]. To handle variants not known + * to this version of the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = proxyConfig.accept(new ProxyConfig.Visitor>() { + * @Override + * public Optional visitBrowserbase(Browserbase browserbase) { + * return Optional.of(browserbase.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in + * [visitor] and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + browserbase != null -> visitor.visitBrowserbase(browserbase) + external != null -> visitor.visitExternal(external) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): ProxyConfig = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitBrowserbase(browserbase: Browserbase) { + browserbase.validate() + } + + override fun visitExternal(external: External) { + external.validate() + } + } + ) + 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 = + accept( + object : Visitor { + override fun visitBrowserbase(browserbase: Browserbase) = + browserbase.validity() + + override fun visitExternal(external: External) = external.validity() + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is ProxyConfig && + browserbase == other.browserbase && + external == other.external + } + + override fun hashCode(): Int = Objects.hash(browserbase, external) + + override fun toString(): String = + when { + browserbase != null -> "ProxyConfig{browserbase=$browserbase}" + external != null -> "ProxyConfig{external=$external}" + _json != null -> "ProxyConfig{_unknown=$_json}" + else -> throw IllegalStateException("Invalid ProxyConfig") + } + + companion object { + + @JvmStatic + fun ofBrowserbase(browserbase: Browserbase) = + ProxyConfig(browserbase = browserbase) + + @JvmStatic fun ofExternal(external: External) = ProxyConfig(external = external) + } + + /** + * An interface that defines how to map each variant of [ProxyConfig] to a value of + * type [T]. + */ + interface Visitor { + + fun visitBrowserbase(browserbase: Browserbase): T + + fun visitExternal(external: External): T + + /** + * Maps an unknown variant of [ProxyConfig] to a value of type [T]. + * + * An instance of [ProxyConfig] can contain an unknown variant if it was + * deserialized from data that doesn't match any known variant. For example, if + * the SDK is on an older version than the API, then the API may respond with + * new variants that the SDK is unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown ProxyConfig: $json") + } + } + + internal class Deserializer : BaseDeserializer(ProxyConfig::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): ProxyConfig { + val json = JsonValue.fromJsonNode(node) + val type = json.asObject().getOrNull()?.get("type")?.asString()?.getOrNull() + + when (type) { + "browserbase" -> { + return tryDeserialize(node, jacksonTypeRef())?.let { + ProxyConfig(browserbase = it, _json = json) + } ?: ProxyConfig(_json = json) + } + "external" -> { + return tryDeserialize(node, jacksonTypeRef())?.let { + ProxyConfig(external = it, _json = json) + } ?: ProxyConfig(_json = json) + } + } + + return ProxyConfig(_json = json) + } + } + + internal class Serializer : BaseSerializer(ProxyConfig::class) { + + override fun serialize( + value: ProxyConfig, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.browserbase != null -> generator.writeObject(value.browserbase) + value.external != null -> generator.writeObject(value.external) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid ProxyConfig") + } + } + } + + class Browserbase + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val type: JsonValue, + private val domainPattern: JsonField, + private val geolocation: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("type") @ExcludeMissing type: JsonValue = JsonMissing.of(), + @JsonProperty("domainPattern") + @ExcludeMissing + domainPattern: JsonField = JsonMissing.of(), + @JsonProperty("geolocation") + @ExcludeMissing + geolocation: JsonField = JsonMissing.of(), + ) : this(type, domainPattern, geolocation, mutableMapOf()) + + /** + * Expected to always return the following: + * ```java + * JsonValue.from("browserbase") + * ``` + * + * However, this method can be useful for debugging and logging (e.g. if the + * server responded with an unexpected value). + */ + @JsonProperty("type") @ExcludeMissing fun _type(): JsonValue = type + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun domainPattern(): Optional = + domainPattern.getOptional("domainPattern") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun geolocation(): Optional = + geolocation.getOptional("geolocation") + + /** + * Returns the raw JSON value of [domainPattern]. + * + * Unlike [domainPattern], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("domainPattern") + @ExcludeMissing + fun _domainPattern(): JsonField = domainPattern + + /** + * Returns the raw JSON value of [geolocation]. + * + * Unlike [geolocation], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("geolocation") + @ExcludeMissing + fun _geolocation(): JsonField = geolocation + + @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 [Browserbase]. + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Browserbase]. */ + class Builder internal constructor() { + + private var type: JsonValue = JsonValue.from("browserbase") + private var domainPattern: JsonField = JsonMissing.of() + private var geolocation: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = + mutableMapOf() + + @JvmSynthetic + internal fun from(browserbase: Browserbase) = apply { + type = browserbase.type + domainPattern = browserbase.domainPattern + geolocation = browserbase.geolocation + additionalProperties = browserbase.additionalProperties.toMutableMap() + } + + /** + * Sets the field to an arbitrary JSON value. + * + * It is usually unnecessary to call this method because the field defaults + * to the following: + * ```java + * JsonValue.from("browserbase") + * ``` + * + * This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun type(type: JsonValue) = apply { this.type = type } + + fun domainPattern(domainPattern: String) = + domainPattern(JsonField.of(domainPattern)) + + /** + * Sets [Builder.domainPattern] to an arbitrary JSON value. + * + * You should usually call [Builder.domainPattern] with a well-typed + * [String] value instead. This method is primarily for setting the field to + * an undocumented or not yet supported value. + */ + fun domainPattern(domainPattern: JsonField) = apply { + this.domainPattern = domainPattern + } + + fun geolocation(geolocation: Geolocation) = + geolocation(JsonField.of(geolocation)) + + /** + * Sets [Builder.geolocation] to an arbitrary JSON value. + * + * You should usually call [Builder.geolocation] with a well-typed + * [Geolocation] value instead. This method is primarily for setting the + * field to an undocumented or not yet supported value. + */ + fun geolocation(geolocation: JsonField) = apply { + this.geolocation = geolocation + } + + 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 [Browserbase]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Browserbase = + Browserbase( + type, + domainPattern, + geolocation, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): Browserbase = apply { + if (validated) { + return@apply + } + + _type().let { + if (it != JsonValue.from("browserbase")) { + throw StagehandInvalidDataException( + "'type' is invalid, received $it" + ) + } + } + domainPattern() + geolocation().ifPresent { it.validate() } + 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 = + type.let { if (it == JsonValue.from("browserbase")) 1 else 0 } + + (if (domainPattern.asKnown().isPresent) 1 else 0) + + (geolocation.asKnown().getOrNull()?.validity() ?: 0) + + class Geolocation + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val country: JsonField, + private val city: JsonField, + private val state: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("country") + @ExcludeMissing + country: JsonField = JsonMissing.of(), + @JsonProperty("city") + @ExcludeMissing + city: JsonField = JsonMissing.of(), + @JsonProperty("state") + @ExcludeMissing + state: JsonField = JsonMissing.of(), + ) : this(country, city, state, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type or is unexpectedly missing or null (e.g. if the server responded + * with an unexpected value). + */ + fun country(): String = country.getRequired("country") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun city(): Optional = city.getOptional("city") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun state(): Optional = state.getOptional("state") + + /** + * Returns the raw JSON value of [country]. + * + * Unlike [country], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("country") + @ExcludeMissing + fun _country(): JsonField = country + + /** + * Returns the raw JSON value of [city]. + * + * Unlike [city], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("city") @ExcludeMissing fun _city(): JsonField = city + + /** + * Returns the raw JSON value of [state]. + * + * Unlike [state], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("state") + @ExcludeMissing + fun _state(): JsonField = state + + @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 + * [Geolocation]. + * + * The following fields are required: + * ```java + * .country() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Geolocation]. */ + class Builder internal constructor() { + + private var country: JsonField? = null + private var city: JsonField = JsonMissing.of() + private var state: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = + mutableMapOf() + + @JvmSynthetic + internal fun from(geolocation: Geolocation) = apply { + country = geolocation.country + city = geolocation.city + state = geolocation.state + additionalProperties = + geolocation.additionalProperties.toMutableMap() + } + + fun country(country: String) = country(JsonField.of(country)) + + /** + * Sets [Builder.country] to an arbitrary JSON value. + * + * You should usually call [Builder.country] with a well-typed [String] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun country(country: JsonField) = apply { + this.country = country + } + + fun city(city: String) = city(JsonField.of(city)) + + /** + * Sets [Builder.city] to an arbitrary JSON value. + * + * You should usually call [Builder.city] with a well-typed [String] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun city(city: JsonField) = apply { this.city = city } + + fun state(state: String) = state(JsonField.of(state)) + + /** + * Sets [Builder.state] to an arbitrary JSON value. + * + * You should usually call [Builder.state] with a well-typed [String] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun state(state: JsonField) = apply { this.state = state } + + 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 [Geolocation]. + * + * Further updates to this [Builder] will not mutate the returned + * instance. + * + * The following fields are required: + * ```java + * .country() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Geolocation = + Geolocation( + checkRequired("country", country), + city, + state, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their + * expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): Geolocation = apply { + if (validated) { + return@apply + } + + country() + city() + state() + 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 = + (if (country.asKnown().isPresent) 1 else 0) + + (if (city.asKnown().isPresent) 1 else 0) + + (if (state.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Geolocation && + country == other.country && + city == other.city && + state == other.state && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(country, city, state, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Geolocation{country=$country, city=$city, state=$state, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Browserbase && + type == other.type && + domainPattern == other.domainPattern && + geolocation == other.geolocation && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(type, domainPattern, geolocation, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Browserbase{type=$type, domainPattern=$domainPattern, geolocation=$geolocation, additionalProperties=$additionalProperties}" + } + + class External + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val server: JsonField, + private val type: JsonValue, + private val domainPattern: JsonField, + private val password: JsonField, + private val username: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("server") + @ExcludeMissing + server: JsonField = JsonMissing.of(), + @JsonProperty("type") @ExcludeMissing type: JsonValue = JsonMissing.of(), + @JsonProperty("domainPattern") + @ExcludeMissing + domainPattern: JsonField = JsonMissing.of(), + @JsonProperty("password") + @ExcludeMissing + password: JsonField = JsonMissing.of(), + @JsonProperty("username") + @ExcludeMissing + username: JsonField = JsonMissing.of(), + ) : this(server, type, domainPattern, password, username, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type or is unexpectedly missing or null (e.g. if the server responded with + * an unexpected value). + */ + fun server(): String = server.getRequired("server") + + /** + * Expected to always return the following: + * ```java + * JsonValue.from("external") + * ``` + * + * However, this method can be useful for debugging and logging (e.g. if the + * server responded with an unexpected value). + */ + @JsonProperty("type") @ExcludeMissing fun _type(): JsonValue = type + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun domainPattern(): Optional = + domainPattern.getOptional("domainPattern") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun password(): Optional = password.getOptional("password") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun username(): Optional = username.getOptional("username") + + /** + * Returns the raw JSON value of [server]. + * + * Unlike [server], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("server") + @ExcludeMissing + fun _server(): JsonField = server + + /** + * Returns the raw JSON value of [domainPattern]. + * + * Unlike [domainPattern], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("domainPattern") + @ExcludeMissing + fun _domainPattern(): JsonField = domainPattern + + /** + * Returns the raw JSON value of [password]. + * + * Unlike [password], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("password") + @ExcludeMissing + fun _password(): JsonField = password + + /** + * Returns the raw JSON value of [username]. + * + * Unlike [username], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("username") + @ExcludeMissing + fun _username(): JsonField = username + + @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 [External]. + * + * The following fields are required: + * ```java + * .server() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [External]. */ + class Builder internal constructor() { + + private var server: JsonField? = null + private var type: JsonValue = JsonValue.from("external") + private var domainPattern: JsonField = JsonMissing.of() + private var password: JsonField = JsonMissing.of() + private var username: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = + mutableMapOf() + + @JvmSynthetic + internal fun from(external: External) = apply { + server = external.server + type = external.type + domainPattern = external.domainPattern + password = external.password + username = external.username + additionalProperties = external.additionalProperties.toMutableMap() + } + + fun server(server: String) = server(JsonField.of(server)) + + /** + * Sets [Builder.server] to an arbitrary JSON value. + * + * You should usually call [Builder.server] with a well-typed [String] value + * instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun server(server: JsonField) = apply { this.server = server } + + /** + * Sets the field to an arbitrary JSON value. + * + * It is usually unnecessary to call this method because the field defaults + * to the following: + * ```java + * JsonValue.from("external") + * ``` + * + * This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun type(type: JsonValue) = apply { this.type = type } + + fun domainPattern(domainPattern: String) = + domainPattern(JsonField.of(domainPattern)) + + /** + * Sets [Builder.domainPattern] to an arbitrary JSON value. + * + * You should usually call [Builder.domainPattern] with a well-typed + * [String] value instead. This method is primarily for setting the field to + * an undocumented or not yet supported value. + */ + fun domainPattern(domainPattern: JsonField) = apply { + this.domainPattern = domainPattern + } + + fun password(password: String) = password(JsonField.of(password)) + + /** + * Sets [Builder.password] to an arbitrary JSON value. + * + * You should usually call [Builder.password] with a well-typed [String] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun password(password: JsonField) = apply { + this.password = password + } + + fun username(username: String) = username(JsonField.of(username)) + + /** + * Sets [Builder.username] to an arbitrary JSON value. + * + * You should usually call [Builder.username] with a well-typed [String] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun username(username: JsonField) = apply { + this.username = username + } + + 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 [External]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .server() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): External = + External( + checkRequired("server", server), + type, + domainPattern, + password, + username, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected + * types recursively. + * + * This method is _not_ forwards compatible with new types from the API for + * existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object + * doesn't match its expected type. + */ + fun validate(): External = apply { + if (validated) { + return@apply + } + + server() + _type().let { + if (it != JsonValue.from("external")) { + throw StagehandInvalidDataException( + "'type' is invalid, received $it" + ) + } + } + domainPattern() + password() + username() + 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 = + (if (server.asKnown().isPresent) 1 else 0) + + type.let { if (it == JsonValue.from("external")) 1 else 0 } + + (if (domainPattern.asKnown().isPresent) 1 else 0) + + (if (password.asKnown().isPresent) 1 else 0) + + (if (username.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is External && + server == other.server && + type == other.type && + domainPattern == other.domainPattern && + password == other.password && + username == other.username && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + server, + type, + domainPattern, + password, + username, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "External{server=$server, type=$type, domainPattern=$domainPattern, password=$password, username=$username, additionalProperties=$additionalProperties}" + } + } + } + + class Region @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is + * on an older version than the API, then the API may respond with new members that the + * SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val US_WEST_2 = of("us-west-2") + + @JvmField val US_EAST_1 = of("us-east-1") + + @JvmField val EU_CENTRAL_1 = of("eu-central-1") + + @JvmField val AP_SOUTHEAST_1 = of("ap-southeast-1") + + @JvmStatic fun of(value: String) = Region(JsonField.of(value)) + } + + /** An enum containing [Region]'s known values. */ + enum class Known { + US_WEST_2, + US_EAST_1, + EU_CENTRAL_1, + AP_SOUTHEAST_1, + } + + /** + * An enum containing [Region]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Region] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + US_WEST_2, + US_EAST_1, + EU_CENTRAL_1, + AP_SOUTHEAST_1, + /** + * An enum member indicating that [Region] was instantiated with an unknown value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you + * want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + US_WEST_2 -> Value.US_WEST_2 + US_EAST_1 -> Value.US_EAST_1 + EU_CENTRAL_1 -> Value.EU_CENTRAL_1 + AP_SOUTHEAST_1 -> Value.AP_SOUTHEAST_1 + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + US_WEST_2 -> Known.US_WEST_2 + US_EAST_1 -> Known.US_EAST_1 + EU_CENTRAL_1 -> Known.EU_CENTRAL_1 + AP_SOUTHEAST_1 -> Known.AP_SOUTHEAST_1 + else -> throw StagehandInvalidDataException("Unknown Region: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have + * the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): Region = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Region && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + class UserMetadata + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [UserMetadata]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [UserMetadata]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(userMetadata: UserMetadata) = apply { + additionalProperties = userMetadata.additionalProperties.toMutableMap() + } + + 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 [UserMetadata]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): UserMetadata = UserMetadata(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): UserMetadata = 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 = + additionalProperties.count { (_, value) -> !value.isNull() && !value.isMissing() } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is UserMetadata && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "UserMetadata{additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is BrowserbaseSessionCreateParams && + browserSettings == other.browserSettings && + extensionId == other.extensionId && + keepAlive == other.keepAlive && + projectId == other.projectId && + proxies == other.proxies && + region == other.region && + timeout == other.timeout && + userMetadata == other.userMetadata && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + browserSettings, + extensionId, + keepAlive, + projectId, + proxies, + region, + timeout, + userMetadata, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "BrowserbaseSessionCreateParams{browserSettings=$browserSettings, extensionId=$extensionId, keepAlive=$keepAlive, projectId=$projectId, proxies=$proxies, region=$region, timeout=$timeout, userMetadata=$userMetadata, additionalProperties=$additionalProperties}" + } + + /** Logging verbosity level (0=quiet, 1=normal, 2=debug) */ + class Verbose @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val _0 = of(0.0) + + @JvmField val _1 = of(1.0) + + @JvmField val _2 = of(2.0) + + @JvmStatic fun of(value: Double) = Verbose(JsonField.of(value)) + } + + /** An enum containing [Verbose]'s known values. */ + enum class Known { + _0, + _1, + _2, + } + + /** + * An enum containing [Verbose]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Verbose] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + _0, + _1, + _2, + /** An enum member indicating that [Verbose] was instantiated with an unknown value. */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + _0 -> Value._0 + _1 -> Value._1 + _2 -> Value._2 + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + _0 -> Known._0 + _1 -> Known._1 + _2 -> Known._2 + else -> throw StagehandInvalidDataException("Unknown Verbose: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asDouble(): Double = + _value().asNumber().getOrNull()?.toDouble() + ?: throw StagehandInvalidDataException("Value is not a Double") + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Verbose = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Verbose && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + /** Whether to stream the response via SSE */ + class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val TRUE = of("true") + + @JvmField val FALSE = of("false") + + @JvmStatic fun of(value: String) = XStreamResponse(JsonField.of(value)) + } + + /** An enum containing [XStreamResponse]'s known values. */ + enum class Known { + TRUE, + FALSE, + } + + /** + * An enum containing [XStreamResponse]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [XStreamResponse] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + TRUE, + FALSE, + /** + * An enum member indicating that [XStreamResponse] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + TRUE -> Value.TRUE + FALSE -> Value.FALSE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + TRUE -> Known.TRUE + FALSE -> Known.FALSE + else -> throw StagehandInvalidDataException("Unknown XStreamResponse: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): XStreamResponse = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is XStreamResponse && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionStartParams && + xStreamResponse == other.xStreamResponse && + body == other.body && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams + } + + override fun hashCode(): Int = + Objects.hash(xStreamResponse, body, additionalHeaders, additionalQueryParams) + + override fun toString() = + "SessionStartParams{xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartResponse.kt new file mode 100644 index 0000000..423a237 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartResponse.kt @@ -0,0 +1,469 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +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.checkRequired +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.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class SessionStartResponse +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val data: JsonField, + private val success: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("success") @ExcludeMissing success: JsonField = JsonMissing.of(), + ) : this(data, success, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun data(): Data = data.getRequired("data") + + /** + * Indicates whether the request was successful + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [data]. + * + * Unlike [data], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("data") @ExcludeMissing fun _data(): JsonField = data + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @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 [SessionStartResponse]. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionStartResponse]. */ + class Builder internal constructor() { + + private var data: JsonField? = null + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(sessionStartResponse: SessionStartResponse) = apply { + data = sessionStartResponse.data + success = sessionStartResponse.success + additionalProperties = sessionStartResponse.additionalProperties.toMutableMap() + } + + fun data(data: Data) = data(JsonField.of(data)) + + /** + * Sets [Builder.data] to an arbitrary JSON value. + * + * You should usually call [Builder.data] with a well-typed [Data] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun data(data: JsonField) = apply { this.data = data } + + /** Indicates whether the request was successful */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [SessionStartResponse]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionStartResponse = + SessionStartResponse( + checkRequired("data", data), + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): SessionStartResponse = apply { + if (validated) { + return@apply + } + + data().validate() + success() + 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 = + (data.asKnown().getOrNull()?.validity() ?: 0) + (if (success.asKnown().isPresent) 1 else 0) + + class Data + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val available: JsonField, + private val sessionId: JsonField, + private val cdpUrl: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("available") + @ExcludeMissing + available: JsonField = JsonMissing.of(), + @JsonProperty("sessionId") + @ExcludeMissing + sessionId: JsonField = JsonMissing.of(), + @JsonProperty("cdpUrl") @ExcludeMissing cdpUrl: JsonField = JsonMissing.of(), + ) : this(available, sessionId, cdpUrl, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun available(): Boolean = available.getRequired("available") + + /** + * Unique Browserbase session identifier + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun sessionId(): String = sessionId.getRequired("sessionId") + + /** + * CDP WebSocket URL for connecting to the Browserbase cloud browser (present when + * available) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun cdpUrl(): Optional = cdpUrl.getOptional("cdpUrl") + + /** + * Returns the raw JSON value of [available]. + * + * Unlike [available], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("available") @ExcludeMissing fun _available(): JsonField = available + + /** + * Returns the raw JSON value of [sessionId]. + * + * Unlike [sessionId], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("sessionId") @ExcludeMissing fun _sessionId(): JsonField = sessionId + + /** + * Returns the raw JSON value of [cdpUrl]. + * + * Unlike [cdpUrl], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("cdpUrl") @ExcludeMissing fun _cdpUrl(): JsonField = cdpUrl + + @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 [Data]. + * + * The following fields are required: + * ```java + * .available() + * .sessionId() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Data]. */ + class Builder internal constructor() { + + private var available: JsonField? = null + private var sessionId: JsonField? = null + private var cdpUrl: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(data: Data) = apply { + available = data.available + sessionId = data.sessionId + cdpUrl = data.cdpUrl + additionalProperties = data.additionalProperties.toMutableMap() + } + + fun available(available: Boolean) = available(JsonField.of(available)) + + /** + * Sets [Builder.available] to an arbitrary JSON value. + * + * You should usually call [Builder.available] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun available(available: JsonField) = apply { this.available = available } + + /** Unique Browserbase session identifier */ + fun sessionId(sessionId: String) = sessionId(JsonField.of(sessionId)) + + /** + * Sets [Builder.sessionId] to an arbitrary JSON value. + * + * You should usually call [Builder.sessionId] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun sessionId(sessionId: JsonField) = apply { this.sessionId = sessionId } + + /** + * CDP WebSocket URL for connecting to the Browserbase cloud browser (present when + * available) + */ + fun cdpUrl(cdpUrl: String?) = cdpUrl(JsonField.ofNullable(cdpUrl)) + + /** Alias for calling [Builder.cdpUrl] with `cdpUrl.orElse(null)`. */ + fun cdpUrl(cdpUrl: Optional) = cdpUrl(cdpUrl.getOrNull()) + + /** + * Sets [Builder.cdpUrl] to an arbitrary JSON value. + * + * You should usually call [Builder.cdpUrl] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun cdpUrl(cdpUrl: JsonField) = apply { this.cdpUrl = cdpUrl } + + 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 [Data]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .available() + * .sessionId() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): Data = + Data( + checkRequired("available", available), + checkRequired("sessionId", sessionId), + cdpUrl, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Data = apply { + if (validated) { + return@apply + } + + available() + sessionId() + cdpUrl() + 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 = + (if (available.asKnown().isPresent) 1 else 0) + + (if (sessionId.asKnown().isPresent) 1 else 0) + + (if (cdpUrl.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Data && + available == other.available && + sessionId == other.sessionId && + cdpUrl == other.cdpUrl && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(available, sessionId, cdpUrl, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Data{available=$available, sessionId=$sessionId, cdpUrl=$cdpUrl, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionStartResponse && + data == other.data && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(data, success, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "SessionStartResponse{data=$data, success=$success, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/StreamEvent.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/StreamEvent.kt new file mode 100644 index 0000000..3c0b838 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/StreamEvent.kt @@ -0,0 +1,1271 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.BaseDeserializer +import com.browserbase.api.core.BaseSerializer +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.allMaxBy +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.getOrThrow +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 com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import java.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** + * Server-Sent Event emitted during streaming responses. Events are sent as `event: \ndata: + * \n\n`, where the JSON payload has the shape `{ data, type, id }`. + */ +class StreamEvent +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val id: JsonField, + private val data: JsonField, + private val type: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("id") @ExcludeMissing id: JsonField = JsonMissing.of(), + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("type") @ExcludeMissing type: JsonField = JsonMissing.of(), + ) : this(id, data, type, mutableMapOf()) + + /** + * Unique identifier for this event + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun id(): String = id.getRequired("id") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun data(): Data = data.getRequired("data") + + /** + * Type of stream event - system events or log messages + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun type(): Type = type.getRequired("type") + + /** + * Returns the raw JSON value of [id]. + * + * Unlike [id], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("id") @ExcludeMissing fun _id(): JsonField = id + + /** + * Returns the raw JSON value of [data]. + * + * Unlike [data], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("data") @ExcludeMissing fun _data(): JsonField = data + + /** + * Returns the raw JSON value of [type]. + * + * Unlike [type], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("type") @ExcludeMissing fun _type(): JsonField = type + + @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 [StreamEvent]. + * + * The following fields are required: + * ```java + * .id() + * .data() + * .type() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [StreamEvent]. */ + class Builder internal constructor() { + + private var id: JsonField? = null + private var data: JsonField? = null + private var type: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(streamEvent: StreamEvent) = apply { + id = streamEvent.id + data = streamEvent.data + type = streamEvent.type + additionalProperties = streamEvent.additionalProperties.toMutableMap() + } + + /** Unique identifier for this event */ + fun id(id: String) = id(JsonField.of(id)) + + /** + * Sets [Builder.id] to an arbitrary JSON value. + * + * You should usually call [Builder.id] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun id(id: JsonField) = apply { this.id = id } + + fun data(data: Data) = data(JsonField.of(data)) + + /** + * Sets [Builder.data] to an arbitrary JSON value. + * + * You should usually call [Builder.data] with a well-typed [Data] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun data(data: JsonField) = apply { this.data = data } + + /** + * Alias for calling [data] with + * `Data.ofStreamEventSystemDataOutput(streamEventSystemDataOutput)`. + */ + fun data(streamEventSystemDataOutput: Data.StreamEventSystemDataOutput) = + data(Data.ofStreamEventSystemDataOutput(streamEventSystemDataOutput)) + + /** + * Alias for calling [data] with + * `Data.ofStreamEventLogDataOutput(streamEventLogDataOutput)`. + */ + fun data(streamEventLogDataOutput: Data.StreamEventLogDataOutput) = + data(Data.ofStreamEventLogDataOutput(streamEventLogDataOutput)) + + /** Type of stream event - system events or log messages */ + fun type(type: Type) = type(JsonField.of(type)) + + /** + * Sets [Builder.type] to an arbitrary JSON value. + * + * You should usually call [Builder.type] with a well-typed [Type] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun type(type: JsonField) = apply { this.type = type } + + 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 [StreamEvent]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .id() + * .data() + * .type() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): StreamEvent = + StreamEvent( + checkRequired("id", id), + checkRequired("data", data), + checkRequired("type", type), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): StreamEvent = apply { + if (validated) { + return@apply + } + + id() + data().validate() + type().validate() + 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 = + (if (id.asKnown().isPresent) 1 else 0) + + (data.asKnown().getOrNull()?.validity() ?: 0) + + (type.asKnown().getOrNull()?.validity() ?: 0) + + @JsonDeserialize(using = Data.Deserializer::class) + @JsonSerialize(using = Data.Serializer::class) + class Data + private constructor( + private val streamEventSystemDataOutput: StreamEventSystemDataOutput? = null, + private val streamEventLogDataOutput: StreamEventLogDataOutput? = null, + private val _json: JsonValue? = null, + ) { + + fun streamEventSystemDataOutput(): Optional = + Optional.ofNullable(streamEventSystemDataOutput) + + fun streamEventLogDataOutput(): Optional = + Optional.ofNullable(streamEventLogDataOutput) + + fun isStreamEventSystemDataOutput(): Boolean = streamEventSystemDataOutput != null + + fun isStreamEventLogDataOutput(): Boolean = streamEventLogDataOutput != null + + fun asStreamEventSystemDataOutput(): StreamEventSystemDataOutput = + streamEventSystemDataOutput.getOrThrow("streamEventSystemDataOutput") + + fun asStreamEventLogDataOutput(): StreamEventLogDataOutput = + streamEventLogDataOutput.getOrThrow("streamEventLogDataOutput") + + fun _json(): Optional = Optional.ofNullable(_json) + + /** + * Maps this instance's current variant to a value of type [T] using the given [visitor]. + * + * Note that this method is _not_ forwards compatible with new variants from the API, unless + * [visitor] overrides [Visitor.unknown]. To handle variants not known to this version of + * the SDK gracefully, consider overriding [Visitor.unknown]: + * ```java + * import com.browserbase.api.core.JsonValue; + * import java.util.Optional; + * + * Optional result = data.accept(new Data.Visitor>() { + * @Override + * public Optional visitStreamEventSystemDataOutput(StreamEventSystemDataOutput streamEventSystemDataOutput) { + * return Optional.of(streamEventSystemDataOutput.toString()); + * } + * + * // ... + * + * @Override + * public Optional unknown(JsonValue json) { + * // Or inspect the `json`. + * return Optional.empty(); + * } + * }); + * ``` + * + * @throws StagehandInvalidDataException if [Visitor.unknown] is not overridden in [visitor] + * and the current variant is unknown. + */ + fun accept(visitor: Visitor): T = + when { + streamEventSystemDataOutput != null -> + visitor.visitStreamEventSystemDataOutput(streamEventSystemDataOutput) + streamEventLogDataOutput != null -> + visitor.visitStreamEventLogDataOutput(streamEventLogDataOutput) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Data = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitStreamEventSystemDataOutput( + streamEventSystemDataOutput: StreamEventSystemDataOutput + ) { + streamEventSystemDataOutput.validate() + } + + override fun visitStreamEventLogDataOutput( + streamEventLogDataOutput: StreamEventLogDataOutput + ) { + streamEventLogDataOutput.validate() + } + } + ) + 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 = + accept( + object : Visitor { + override fun visitStreamEventSystemDataOutput( + streamEventSystemDataOutput: StreamEventSystemDataOutput + ) = streamEventSystemDataOutput.validity() + + override fun visitStreamEventLogDataOutput( + streamEventLogDataOutput: StreamEventLogDataOutput + ) = streamEventLogDataOutput.validity() + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Data && + streamEventSystemDataOutput == other.streamEventSystemDataOutput && + streamEventLogDataOutput == other.streamEventLogDataOutput + } + + override fun hashCode(): Int = + Objects.hash(streamEventSystemDataOutput, streamEventLogDataOutput) + + override fun toString(): String = + when { + streamEventSystemDataOutput != null -> + "Data{streamEventSystemDataOutput=$streamEventSystemDataOutput}" + streamEventLogDataOutput != null -> + "Data{streamEventLogDataOutput=$streamEventLogDataOutput}" + _json != null -> "Data{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Data") + } + + companion object { + + @JvmStatic + fun ofStreamEventSystemDataOutput( + streamEventSystemDataOutput: StreamEventSystemDataOutput + ) = Data(streamEventSystemDataOutput = streamEventSystemDataOutput) + + @JvmStatic + fun ofStreamEventLogDataOutput(streamEventLogDataOutput: StreamEventLogDataOutput) = + Data(streamEventLogDataOutput = streamEventLogDataOutput) + } + + /** An interface that defines how to map each variant of [Data] to a value of type [T]. */ + interface Visitor { + + fun visitStreamEventSystemDataOutput( + streamEventSystemDataOutput: StreamEventSystemDataOutput + ): T + + fun visitStreamEventLogDataOutput(streamEventLogDataOutput: StreamEventLogDataOutput): T + + /** + * Maps an unknown variant of [Data] to a value of type [T]. + * + * An instance of [Data] can contain an unknown variant if it was deserialized from data + * that doesn't match any known variant. For example, if the SDK is on an older version + * than the API, then the API may respond with new variants that the SDK is unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Data: $json") + } + } + + internal class Deserializer : BaseDeserializer(Data::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Data { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef()) + ?.let { Data(streamEventSystemDataOutput = it, _json = json) }, + tryDeserialize(node, jacksonTypeRef())?.let { + Data(streamEventLogDataOutput = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible with + // all the possible variants (e.g. deserializing from boolean). + 0 -> Data(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the first + // completely valid match, or simply the first match if none are completely + // valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Data::class) { + + override fun serialize( + value: Data, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.streamEventSystemDataOutput != null -> + generator.writeObject(value.streamEventSystemDataOutput) + value.streamEventLogDataOutput != null -> + generator.writeObject(value.streamEventLogDataOutput) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Data") + } + } + } + + class StreamEventSystemDataOutput + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val status: JsonField, + private val error: JsonField, + private val result: JsonValue, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("status") + @ExcludeMissing + status: JsonField = JsonMissing.of(), + @JsonProperty("error") @ExcludeMissing error: JsonField = JsonMissing.of(), + @JsonProperty("result") @ExcludeMissing result: JsonValue = JsonMissing.of(), + ) : this(status, error, result, mutableMapOf()) + + /** + * Current status of the streaming operation + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun status(): Status = status.getRequired("status") + + /** + * Error message (present when status is 'error') + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun error(): Optional = error.getOptional("error") + + /** + * Operation result (present when status is 'finished') + * + * This arbitrary value can be deserialized into a custom type using the `convert` + * method: + * ```java + * MyClass myObject = streamEventSystemDataOutput.result().convert(MyClass.class); + * ``` + */ + @JsonProperty("result") @ExcludeMissing fun _result(): JsonValue = result + + /** + * Returns the raw JSON value of [status]. + * + * Unlike [status], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("status") @ExcludeMissing fun _status(): JsonField = status + + /** + * Returns the raw JSON value of [error]. + * + * Unlike [error], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("error") @ExcludeMissing fun _error(): JsonField = error + + @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 + * [StreamEventSystemDataOutput]. + * + * The following fields are required: + * ```java + * .status() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [StreamEventSystemDataOutput]. */ + class Builder internal constructor() { + + private var status: JsonField? = null + private var error: JsonField = JsonMissing.of() + private var result: JsonValue = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(streamEventSystemDataOutput: StreamEventSystemDataOutput) = + apply { + status = streamEventSystemDataOutput.status + error = streamEventSystemDataOutput.error + result = streamEventSystemDataOutput.result + additionalProperties = + streamEventSystemDataOutput.additionalProperties.toMutableMap() + } + + /** Current status of the streaming operation */ + fun status(status: Status) = status(JsonField.of(status)) + + /** + * Sets [Builder.status] to an arbitrary JSON value. + * + * You should usually call [Builder.status] with a well-typed [Status] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun status(status: JsonField) = apply { this.status = status } + + /** Error message (present when status is 'error') */ + fun error(error: String) = error(JsonField.of(error)) + + /** + * Sets [Builder.error] to an arbitrary JSON value. + * + * You should usually call [Builder.error] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun error(error: JsonField) = apply { this.error = error } + + /** Operation result (present when status is 'finished') */ + fun result(result: JsonValue) = apply { this.result = result } + + 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 [StreamEventSystemDataOutput]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .status() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): StreamEventSystemDataOutput = + StreamEventSystemDataOutput( + checkRequired("status", status), + error, + result, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): StreamEventSystemDataOutput = apply { + if (validated) { + return@apply + } + + status().validate() + error() + 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 = + (status.asKnown().getOrNull()?.validity() ?: 0) + + (if (error.asKnown().isPresent) 1 else 0) + + /** Current status of the streaming operation */ + class Status @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that + * doesn't match any known member, and you want to know that value. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val STARTING = of("starting") + + @JvmField val CONNECTED = of("connected") + + @JvmField val RUNNING = of("running") + + @JvmField val FINISHED = of("finished") + + @JvmField val ERROR = of("error") + + @JvmStatic fun of(value: String) = Status(JsonField.of(value)) + } + + /** An enum containing [Status]'s known values. */ + enum class Known { + STARTING, + CONNECTED, + RUNNING, + FINISHED, + ERROR, + } + + /** + * An enum containing [Status]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Status] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, + * if the SDK is on an older version than the API, then the API may respond with + * new members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + STARTING, + CONNECTED, + RUNNING, + FINISHED, + ERROR, + /** + * An enum member indicating that [Status] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if + * you want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + STARTING -> Value.STARTING + CONNECTED -> Value.CONNECTED + RUNNING -> Value.RUNNING + FINISHED -> Value.FINISHED + ERROR -> Value.ERROR + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a + * known member. + */ + fun known(): Known = + when (this) { + STARTING -> Known.STARTING + CONNECTED -> Known.CONNECTED + RUNNING -> Known.RUNNING + FINISHED -> Known.FINISHED + ERROR -> Known.ERROR + else -> throw StagehandInvalidDataException("Unknown Status: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not + * have the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't + * match its expected type. + */ + fun validate(): Status = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Status && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is StreamEventSystemDataOutput && + status == other.status && + error == other.error && + result == other.result && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(status, error, result, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "StreamEventSystemDataOutput{status=$status, error=$error, result=$result, additionalProperties=$additionalProperties}" + } + + class StreamEventLogDataOutput + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val message: JsonField, + private val status: JsonValue, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("message") + @ExcludeMissing + message: JsonField = JsonMissing.of(), + @JsonProperty("status") @ExcludeMissing status: JsonValue = JsonMissing.of(), + ) : this(message, status, mutableMapOf()) + + /** + * Log message from the operation + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun message(): String = message.getRequired("message") + + /** + * Expected to always return the following: + * ```java + * JsonValue.from("running") + * ``` + * + * However, this method can be useful for debugging and logging (e.g. if the server + * responded with an unexpected value). + */ + @JsonProperty("status") @ExcludeMissing fun _status(): JsonValue = status + + /** + * Returns the raw JSON value of [message]. + * + * Unlike [message], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("message") @ExcludeMissing fun _message(): JsonField = message + + @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 + * [StreamEventLogDataOutput]. + * + * The following fields are required: + * ```java + * .message() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [StreamEventLogDataOutput]. */ + class Builder internal constructor() { + + private var message: JsonField? = null + private var status: JsonValue = JsonValue.from("running") + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(streamEventLogDataOutput: StreamEventLogDataOutput) = apply { + message = streamEventLogDataOutput.message + status = streamEventLogDataOutput.status + additionalProperties = + streamEventLogDataOutput.additionalProperties.toMutableMap() + } + + /** Log message from the operation */ + fun message(message: String) = message(JsonField.of(message)) + + /** + * Sets [Builder.message] to an arbitrary JSON value. + * + * You should usually call [Builder.message] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun message(message: JsonField) = apply { this.message = message } + + /** + * Sets the field to an arbitrary JSON value. + * + * It is usually unnecessary to call this method because the field defaults to the + * following: + * ```java + * JsonValue.from("running") + * ``` + * + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun status(status: JsonValue) = apply { this.status = status } + + 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 [StreamEventLogDataOutput]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .message() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): StreamEventLogDataOutput = + StreamEventLogDataOutput( + checkRequired("message", message), + status, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing + * fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match + * its expected type. + */ + fun validate(): StreamEventLogDataOutput = apply { + if (validated) { + return@apply + } + + message() + _status().let { + if (it != JsonValue.from("running")) { + throw StagehandInvalidDataException("'status' is invalid, received $it") + } + } + 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 = + (if (message.asKnown().isPresent) 1 else 0) + + status.let { if (it == JsonValue.from("running")) 1 else 0 } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is StreamEventLogDataOutput && + message == other.message && + status == other.status && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(message, status, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "StreamEventLogDataOutput{message=$message, status=$status, additionalProperties=$additionalProperties}" + } + } + + /** Type of stream event - system events or log messages */ + class Type @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val SYSTEM = of("system") + + @JvmField val LOG = of("log") + + @JvmStatic fun of(value: String) = Type(JsonField.of(value)) + } + + /** An enum containing [Type]'s known values. */ + enum class Known { + SYSTEM, + LOG, + } + + /** + * An enum containing [Type]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Type] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + SYSTEM, + LOG, + /** An enum member indicating that [Type] was instantiated with an unknown value. */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + SYSTEM -> Value.SYSTEM + LOG -> Value.LOG + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + SYSTEM -> Known.SYSTEM + LOG -> Known.LOG + else -> throw StagehandInvalidDataException("Unknown Type: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + /** + * Validates that the types of all values in this object match their expected types + * recursively. + * + * This method is _not_ forwards compatible with new types from the API for existing fields. + * + * @throws StagehandInvalidDataException if any value type in this object doesn't match its + * expected type. + */ + fun validate(): Type = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Type && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is StreamEvent && + id == other.id && + data == other.data && + type == other.type && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(id, data, type, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "StreamEvent{id=$id, data=$data, type=$type, additionalProperties=$additionalProperties}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsync.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsync.kt new file mode 100644 index 0000000..f9e65e6 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsync.kt @@ -0,0 +1,835 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.services.async + +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.core.RequestOptions +import com.browserbase.api.core.http.AsyncStreamResponse +import com.browserbase.api.core.http.HttpResponseFor +import com.browserbase.api.core.http.StreamResponse +import com.browserbase.api.models.sessions.SessionActParams +import com.browserbase.api.models.sessions.SessionActResponse +import com.browserbase.api.models.sessions.SessionEndParams +import com.browserbase.api.models.sessions.SessionEndResponse +import com.browserbase.api.models.sessions.SessionExecuteParams +import com.browserbase.api.models.sessions.SessionExecuteResponse +import com.browserbase.api.models.sessions.SessionExtractParams +import com.browserbase.api.models.sessions.SessionExtractResponse +import com.browserbase.api.models.sessions.SessionNavigateParams +import com.browserbase.api.models.sessions.SessionNavigateResponse +import com.browserbase.api.models.sessions.SessionObserveParams +import com.browserbase.api.models.sessions.SessionObserveResponse +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionReplayResponse +import com.browserbase.api.models.sessions.SessionStartParams +import com.browserbase.api.models.sessions.SessionStartResponse +import com.browserbase.api.models.sessions.StreamEvent +import com.google.errorprone.annotations.MustBeClosed +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer + +interface SessionServiceAsync { + + /** + * Returns a view of this service that provides access to raw HTTP responses for each method. + */ + fun withRawResponse(): WithRawResponse + + /** + * Returns a view of this service with the given option modifications applied. + * + * The original service is not modified. + */ + fun withOptions(modifier: Consumer): SessionServiceAsync + + /** + * Executes a browser action using natural language instructions or a predefined Action object. + */ + fun act(id: String, params: SessionActParams): CompletableFuture = + act(id, params, RequestOptions.none()) + + /** @see act */ + fun act( + id: String, + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + act(params.toBuilder().id(id).build(), requestOptions) + + /** @see act */ + fun act(params: SessionActParams): CompletableFuture = + act(params, RequestOptions.none()) + + /** @see act */ + fun act( + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** + * Executes a browser action using natural language instructions or a predefined Action object. + */ + fun actStreaming(id: String, params: SessionActParams): AsyncStreamResponse = + actStreaming(id, params, RequestOptions.none()) + + /** @see actStreaming */ + fun actStreaming( + id: String, + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): AsyncStreamResponse = + actStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see actStreaming */ + fun actStreaming(params: SessionActParams): AsyncStreamResponse = + actStreaming(params, RequestOptions.none()) + + /** @see actStreaming */ + fun actStreaming( + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): AsyncStreamResponse + + /** Terminates the browser session and releases all associated resources. */ + fun end(id: String): CompletableFuture = end(id, SessionEndParams.none()) + + /** @see end */ + fun end( + id: String, + params: SessionEndParams = SessionEndParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + end(params.toBuilder().id(id).build(), requestOptions) + + /** @see end */ + fun end( + id: String, + params: SessionEndParams = SessionEndParams.none(), + ): CompletableFuture = end(id, params, RequestOptions.none()) + + /** @see end */ + fun end( + params: SessionEndParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** @see end */ + fun end(params: SessionEndParams): CompletableFuture = + end(params, RequestOptions.none()) + + /** @see end */ + fun end(id: String, requestOptions: RequestOptions): CompletableFuture = + end(id, SessionEndParams.none(), requestOptions) + + /** Runs an autonomous AI agent that can perform complex multi-step browser tasks. */ + fun execute( + id: String, + params: SessionExecuteParams, + ): CompletableFuture = execute(id, params, RequestOptions.none()) + + /** @see execute */ + fun execute( + id: String, + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + execute(params.toBuilder().id(id).build(), requestOptions) + + /** @see execute */ + fun execute(params: SessionExecuteParams): CompletableFuture = + execute(params, RequestOptions.none()) + + /** @see execute */ + fun execute( + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** Runs an autonomous AI agent that can perform complex multi-step browser tasks. */ + fun executeStreaming( + id: String, + params: SessionExecuteParams, + ): AsyncStreamResponse = executeStreaming(id, params, RequestOptions.none()) + + /** @see executeStreaming */ + fun executeStreaming( + id: String, + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): AsyncStreamResponse = + executeStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see executeStreaming */ + fun executeStreaming(params: SessionExecuteParams): AsyncStreamResponse = + executeStreaming(params, RequestOptions.none()) + + /** @see executeStreaming */ + fun executeStreaming( + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): AsyncStreamResponse + + /** Extracts structured data from the current page using AI-powered analysis. */ + fun extract(id: String): CompletableFuture = + extract(id, SessionExtractParams.none()) + + /** @see extract */ + fun extract( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + extract(params.toBuilder().id(id).build(), requestOptions) + + /** @see extract */ + fun extract( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + ): CompletableFuture = extract(id, params, RequestOptions.none()) + + /** @see extract */ + fun extract( + params: SessionExtractParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** @see extract */ + fun extract(params: SessionExtractParams): CompletableFuture = + extract(params, RequestOptions.none()) + + /** @see extract */ + fun extract( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture = + extract(id, SessionExtractParams.none(), requestOptions) + + /** Extracts structured data from the current page using AI-powered analysis. */ + fun extractStreaming(id: String): AsyncStreamResponse = + extractStreaming(id, SessionExtractParams.none()) + + /** @see extractStreaming */ + fun extractStreaming( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): AsyncStreamResponse = + extractStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see extractStreaming */ + fun extractStreaming( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + ): AsyncStreamResponse = extractStreaming(id, params, RequestOptions.none()) + + /** @see extractStreaming */ + fun extractStreaming( + params: SessionExtractParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): AsyncStreamResponse + + /** @see extractStreaming */ + fun extractStreaming(params: SessionExtractParams): AsyncStreamResponse = + extractStreaming(params, RequestOptions.none()) + + /** @see extractStreaming */ + fun extractStreaming( + id: String, + requestOptions: RequestOptions, + ): AsyncStreamResponse = + extractStreaming(id, SessionExtractParams.none(), requestOptions) + + /** Navigates the browser to the specified URL. */ + fun navigate( + id: String, + params: SessionNavigateParams, + ): CompletableFuture = navigate(id, params, RequestOptions.none()) + + /** @see navigate */ + fun navigate( + id: String, + params: SessionNavigateParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + navigate(params.toBuilder().id(id).build(), requestOptions) + + /** @see navigate */ + fun navigate(params: SessionNavigateParams): CompletableFuture = + navigate(params, RequestOptions.none()) + + /** @see navigate */ + fun navigate( + params: SessionNavigateParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** + * Identifies and returns available actions on the current page that match the given + * instruction. + */ + fun observe(id: String): CompletableFuture = + observe(id, SessionObserveParams.none()) + + /** @see observe */ + fun observe( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + observe(params.toBuilder().id(id).build(), requestOptions) + + /** @see observe */ + fun observe( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + ): CompletableFuture = observe(id, params, RequestOptions.none()) + + /** @see observe */ + fun observe( + params: SessionObserveParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** @see observe */ + fun observe(params: SessionObserveParams): CompletableFuture = + observe(params, RequestOptions.none()) + + /** @see observe */ + fun observe( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture = + observe(id, SessionObserveParams.none(), requestOptions) + + /** + * Identifies and returns available actions on the current page that match the given + * instruction. + */ + fun observeStreaming(id: String): AsyncStreamResponse = + observeStreaming(id, SessionObserveParams.none()) + + /** @see observeStreaming */ + fun observeStreaming( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): AsyncStreamResponse = + observeStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see observeStreaming */ + fun observeStreaming( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + ): AsyncStreamResponse = observeStreaming(id, params, RequestOptions.none()) + + /** @see observeStreaming */ + fun observeStreaming( + params: SessionObserveParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): AsyncStreamResponse + + /** @see observeStreaming */ + fun observeStreaming(params: SessionObserveParams): AsyncStreamResponse = + observeStreaming(params, RequestOptions.none()) + + /** @see observeStreaming */ + fun observeStreaming( + id: String, + requestOptions: RequestOptions, + ): AsyncStreamResponse = + observeStreaming(id, SessionObserveParams.none(), requestOptions) + + /** Retrieves replay metrics for a session. */ + fun replay(id: String): CompletableFuture = + replay(id, SessionReplayParams.none()) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + replay(params.toBuilder().id(id).build(), requestOptions) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + ): CompletableFuture = replay(id, params, RequestOptions.none()) + + /** @see replay */ + fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** @see replay */ + fun replay(params: SessionReplayParams): CompletableFuture = + replay(params, RequestOptions.none()) + + /** @see replay */ + fun replay( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture = + replay(id, SessionReplayParams.none(), requestOptions) + + /** + * Creates a new browser session with the specified configuration. Returns a session ID used for + * all subsequent operations. + */ + fun start(params: SessionStartParams): CompletableFuture = + start(params, RequestOptions.none()) + + /** @see start */ + fun start( + params: SessionStartParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** + * A view of [SessionServiceAsync] that provides access to raw HTTP responses for each method. + */ + interface WithRawResponse { + + /** + * Returns a view of this service with the given option modifications applied. + * + * The original service is not modified. + */ + fun withOptions( + modifier: Consumer + ): SessionServiceAsync.WithRawResponse + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/act`, but is otherwise the same + * as [SessionServiceAsync.act]. + */ + fun act( + id: String, + params: SessionActParams, + ): CompletableFuture> = + act(id, params, RequestOptions.none()) + + /** @see act */ + fun act( + id: String, + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + act(params.toBuilder().id(id).build(), requestOptions) + + /** @see act */ + fun act(params: SessionActParams): CompletableFuture> = + act(params, RequestOptions.none()) + + /** @see act */ + fun act( + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/act`, but is otherwise the same + * as [SessionServiceAsync.actStreaming]. + */ + @MustBeClosed + fun actStreaming( + id: String, + params: SessionActParams, + ): CompletableFuture>> = + actStreaming(id, params, RequestOptions.none()) + + /** @see actStreaming */ + @MustBeClosed + fun actStreaming( + id: String, + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture>> = + actStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see actStreaming */ + @MustBeClosed + fun actStreaming( + params: SessionActParams + ): CompletableFuture>> = + actStreaming(params, RequestOptions.none()) + + /** @see actStreaming */ + @MustBeClosed + fun actStreaming( + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture>> + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/end`, but is otherwise the same + * as [SessionServiceAsync.end]. + */ + fun end(id: String): CompletableFuture> = + end(id, SessionEndParams.none()) + + /** @see end */ + fun end( + id: String, + params: SessionEndParams = SessionEndParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + end(params.toBuilder().id(id).build(), requestOptions) + + /** @see end */ + fun end( + id: String, + params: SessionEndParams = SessionEndParams.none(), + ): CompletableFuture> = + end(id, params, RequestOptions.none()) + + /** @see end */ + fun end( + params: SessionEndParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + + /** @see end */ + fun end(params: SessionEndParams): CompletableFuture> = + end(params, RequestOptions.none()) + + /** @see end */ + fun end( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture> = + end(id, SessionEndParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/agentExecute`, but is otherwise + * the same as [SessionServiceAsync.execute]. + */ + fun execute( + id: String, + params: SessionExecuteParams, + ): CompletableFuture> = + execute(id, params, RequestOptions.none()) + + /** @see execute */ + fun execute( + id: String, + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + execute(params.toBuilder().id(id).build(), requestOptions) + + /** @see execute */ + fun execute( + params: SessionExecuteParams + ): CompletableFuture> = + execute(params, RequestOptions.none()) + + /** @see execute */ + fun execute( + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/agentExecute`, but is otherwise + * the same as [SessionServiceAsync.executeStreaming]. + */ + @MustBeClosed + fun executeStreaming( + id: String, + params: SessionExecuteParams, + ): CompletableFuture>> = + executeStreaming(id, params, RequestOptions.none()) + + /** @see executeStreaming */ + @MustBeClosed + fun executeStreaming( + id: String, + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture>> = + executeStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see executeStreaming */ + @MustBeClosed + fun executeStreaming( + params: SessionExecuteParams + ): CompletableFuture>> = + executeStreaming(params, RequestOptions.none()) + + /** @see executeStreaming */ + @MustBeClosed + fun executeStreaming( + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture>> + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/extract`, but is otherwise the + * same as [SessionServiceAsync.extract]. + */ + fun extract(id: String): CompletableFuture> = + extract(id, SessionExtractParams.none()) + + /** @see extract */ + fun extract( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + extract(params.toBuilder().id(id).build(), requestOptions) + + /** @see extract */ + fun extract( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + ): CompletableFuture> = + extract(id, params, RequestOptions.none()) + + /** @see extract */ + fun extract( + params: SessionExtractParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + + /** @see extract */ + fun extract( + params: SessionExtractParams + ): CompletableFuture> = + extract(params, RequestOptions.none()) + + /** @see extract */ + fun extract( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture> = + extract(id, SessionExtractParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/extract`, but is otherwise the + * same as [SessionServiceAsync.extractStreaming]. + */ + @MustBeClosed + fun extractStreaming( + id: String + ): CompletableFuture>> = + extractStreaming(id, SessionExtractParams.none()) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture>> = + extractStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + ): CompletableFuture>> = + extractStreaming(id, params, RequestOptions.none()) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + params: SessionExtractParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture>> + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + params: SessionExtractParams + ): CompletableFuture>> = + extractStreaming(params, RequestOptions.none()) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture>> = + extractStreaming(id, SessionExtractParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/navigate`, but is otherwise the + * same as [SessionServiceAsync.navigate]. + */ + fun navigate( + id: String, + params: SessionNavigateParams, + ): CompletableFuture> = + navigate(id, params, RequestOptions.none()) + + /** @see navigate */ + fun navigate( + id: String, + params: SessionNavigateParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + navigate(params.toBuilder().id(id).build(), requestOptions) + + /** @see navigate */ + fun navigate( + params: SessionNavigateParams + ): CompletableFuture> = + navigate(params, RequestOptions.none()) + + /** @see navigate */ + fun navigate( + params: SessionNavigateParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/observe`, but is otherwise the + * same as [SessionServiceAsync.observe]. + */ + fun observe(id: String): CompletableFuture> = + observe(id, SessionObserveParams.none()) + + /** @see observe */ + fun observe( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + observe(params.toBuilder().id(id).build(), requestOptions) + + /** @see observe */ + fun observe( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + ): CompletableFuture> = + observe(id, params, RequestOptions.none()) + + /** @see observe */ + fun observe( + params: SessionObserveParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + + /** @see observe */ + fun observe( + params: SessionObserveParams + ): CompletableFuture> = + observe(params, RequestOptions.none()) + + /** @see observe */ + fun observe( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture> = + observe(id, SessionObserveParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/observe`, but is otherwise the + * same as [SessionServiceAsync.observeStreaming]. + */ + @MustBeClosed + fun observeStreaming( + id: String + ): CompletableFuture>> = + observeStreaming(id, SessionObserveParams.none()) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture>> = + observeStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + ): CompletableFuture>> = + observeStreaming(id, params, RequestOptions.none()) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + params: SessionObserveParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture>> + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + params: SessionObserveParams + ): CompletableFuture>> = + observeStreaming(params, RequestOptions.none()) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture>> = + observeStreaming(id, SessionObserveParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `get /v1/sessions/{id}/replay`, but is otherwise the same + * as [SessionServiceAsync.replay]. + */ + fun replay(id: String): CompletableFuture> = + replay(id, SessionReplayParams.none()) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + replay(params.toBuilder().id(id).build(), requestOptions) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + ): CompletableFuture> = + replay(id, params, RequestOptions.none()) + + /** @see replay */ + fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + + /** @see replay */ + fun replay( + params: SessionReplayParams + ): CompletableFuture> = + replay(params, RequestOptions.none()) + + /** @see replay */ + fun replay( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture> = + replay(id, SessionReplayParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/start`, but is otherwise the same as + * [SessionServiceAsync.start]. + */ + fun start( + params: SessionStartParams + ): CompletableFuture> = + start(params, RequestOptions.none()) + + /** @see start */ + fun start( + params: SessionStartParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + } +} 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 new file mode 100644 index 0000000..9a2fdc2 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsyncImpl.kt @@ -0,0 +1,620 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.services.async + +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.RequestOptions +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.handlers.errorBodyHandler +import com.browserbase.api.core.handlers.errorHandler +import com.browserbase.api.core.handlers.jsonHandler +import com.browserbase.api.core.handlers.mapJson +import com.browserbase.api.core.handlers.sseHandler +import com.browserbase.api.core.http.AsyncStreamResponse +import com.browserbase.api.core.http.HttpMethod +import com.browserbase.api.core.http.HttpRequest +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.core.http.HttpResponse.Handler +import com.browserbase.api.core.http.HttpResponseFor +import com.browserbase.api.core.http.StreamResponse +import com.browserbase.api.core.http.json +import com.browserbase.api.core.http.map +import com.browserbase.api.core.http.parseable +import com.browserbase.api.core.http.toAsync +import com.browserbase.api.core.prepareAsync +import com.browserbase.api.models.sessions.SessionActParams +import com.browserbase.api.models.sessions.SessionActResponse +import com.browserbase.api.models.sessions.SessionEndParams +import com.browserbase.api.models.sessions.SessionEndResponse +import com.browserbase.api.models.sessions.SessionExecuteParams +import com.browserbase.api.models.sessions.SessionExecuteResponse +import com.browserbase.api.models.sessions.SessionExtractParams +import com.browserbase.api.models.sessions.SessionExtractResponse +import com.browserbase.api.models.sessions.SessionNavigateParams +import com.browserbase.api.models.sessions.SessionNavigateResponse +import com.browserbase.api.models.sessions.SessionObserveParams +import com.browserbase.api.models.sessions.SessionObserveResponse +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionReplayResponse +import com.browserbase.api.models.sessions.SessionStartParams +import com.browserbase.api.models.sessions.SessionStartResponse +import com.browserbase.api.models.sessions.StreamEvent +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer +import kotlin.jvm.optionals.getOrNull + +class SessionServiceAsyncImpl internal constructor(private val clientOptions: ClientOptions) : + SessionServiceAsync { + + private val withRawResponse: SessionServiceAsync.WithRawResponse by lazy { + WithRawResponseImpl(clientOptions) + } + + override fun withRawResponse(): SessionServiceAsync.WithRawResponse = withRawResponse + + override fun withOptions(modifier: Consumer): SessionServiceAsync = + SessionServiceAsyncImpl(clientOptions.toBuilder().apply(modifier::accept).build()) + + override fun act( + params: SessionActParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // post /v1/sessions/{id}/act + withRawResponse().act(params, requestOptions).thenApply { it.parse() } + + override fun actStreaming( + params: SessionActParams, + requestOptions: RequestOptions, + ): AsyncStreamResponse = + // post /v1/sessions/{id}/act + withRawResponse() + .actStreaming(params, requestOptions) + .thenApply { it.parse() } + .toAsync(clientOptions.streamHandlerExecutor) + + override fun end( + params: SessionEndParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // post /v1/sessions/{id}/end + withRawResponse().end(params, requestOptions).thenApply { it.parse() } + + override fun execute( + params: SessionExecuteParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // post /v1/sessions/{id}/agentExecute + withRawResponse().execute(params, requestOptions).thenApply { it.parse() } + + override fun executeStreaming( + params: SessionExecuteParams, + requestOptions: RequestOptions, + ): AsyncStreamResponse = + // post /v1/sessions/{id}/agentExecute + withRawResponse() + .executeStreaming(params, requestOptions) + .thenApply { it.parse() } + .toAsync(clientOptions.streamHandlerExecutor) + + override fun extract( + params: SessionExtractParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // post /v1/sessions/{id}/extract + withRawResponse().extract(params, requestOptions).thenApply { it.parse() } + + override fun extractStreaming( + params: SessionExtractParams, + requestOptions: RequestOptions, + ): AsyncStreamResponse = + // post /v1/sessions/{id}/extract + withRawResponse() + .extractStreaming(params, requestOptions) + .thenApply { it.parse() } + .toAsync(clientOptions.streamHandlerExecutor) + + override fun navigate( + params: SessionNavigateParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // post /v1/sessions/{id}/navigate + withRawResponse().navigate(params, requestOptions).thenApply { it.parse() } + + override fun observe( + params: SessionObserveParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // post /v1/sessions/{id}/observe + withRawResponse().observe(params, requestOptions).thenApply { it.parse() } + + override fun observeStreaming( + params: SessionObserveParams, + requestOptions: RequestOptions, + ): AsyncStreamResponse = + // post /v1/sessions/{id}/observe + withRawResponse() + .observeStreaming(params, requestOptions) + .thenApply { it.parse() } + .toAsync(clientOptions.streamHandlerExecutor) + + override fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // get /v1/sessions/{id}/replay + withRawResponse().replay(params, requestOptions).thenApply { it.parse() } + + override fun start( + params: SessionStartParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // post /v1/sessions/start + withRawResponse().start(params, requestOptions).thenApply { it.parse() } + + class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) : + SessionServiceAsync.WithRawResponse { + + private val errorHandler: Handler = + errorHandler(errorBodyHandler(clientOptions.jsonMapper)) + + override fun withOptions( + modifier: Consumer + ): SessionServiceAsync.WithRawResponse = + SessionServiceAsyncImpl.WithRawResponseImpl( + clientOptions.toBuilder().apply(modifier::accept).build() + ) + + private val actHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun act( + params: SessionActParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "act") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { actHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + + private val actStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper).mapJson() + + override fun actStreaming( + params: SessionActParams, + requestOptions: RequestOptions, + ): CompletableFuture>> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "act") + .putHeader("Accept", "text/event-stream") + .body( + json( + clientOptions.jsonMapper, + params + ._body() + .toBuilder() + .putAdditionalProperty("streamResponse", JsonValue.from(true)) + .build(), + ) + ) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .let { actStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation!!) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + } + } + + private val endHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun end( + params: SessionEndParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "end") + .apply { params._body().ifPresent { body(json(clientOptions.jsonMapper, it)) } } + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { endHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + + private val executeHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun execute( + params: SessionExecuteParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "agentExecute") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { executeHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + + private val executeStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper).mapJson() + + override fun executeStreaming( + params: SessionExecuteParams, + requestOptions: RequestOptions, + ): CompletableFuture>> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "agentExecute") + .putHeader("Accept", "text/event-stream") + .body( + json( + clientOptions.jsonMapper, + params + ._body() + .toBuilder() + .putAdditionalProperty("streamResponse", JsonValue.from(true)) + .build(), + ) + ) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .let { executeStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation!!) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + } + } + + private val extractHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun extract( + params: SessionExtractParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "extract") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { extractHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + + private val extractStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper).mapJson() + + override fun extractStreaming( + params: SessionExtractParams, + requestOptions: RequestOptions, + ): CompletableFuture>> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "extract") + .putHeader("Accept", "text/event-stream") + .body( + json( + clientOptions.jsonMapper, + params + ._body() + .toBuilder() + .putAdditionalProperty("streamResponse", JsonValue.from(true)) + .build(), + ) + ) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .let { extractStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation!!) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + } + } + + private val navigateHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun navigate( + params: SessionNavigateParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "navigate") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { navigateHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + + private val observeHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun observe( + params: SessionObserveParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "observe") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { observeHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + + private val observeStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper).mapJson() + + override fun observeStreaming( + params: SessionObserveParams, + requestOptions: RequestOptions, + ): CompletableFuture>> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "observe") + .putHeader("Accept", "text/event-stream") + .body( + json( + clientOptions.jsonMapper, + params + ._body() + .toBuilder() + .putAdditionalProperty("streamResponse", JsonValue.from(true)) + .build(), + ) + ) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .let { observeStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation!!) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + } + } + + private val replayHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "replay") + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { replayHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + + private val startHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun start( + params: SessionStartParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", "start") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { startHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + } +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionService.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionService.kt new file mode 100644 index 0000000..1c44d08 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionService.kt @@ -0,0 +1,820 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.services.blocking + +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.core.RequestOptions +import com.browserbase.api.core.http.HttpResponseFor +import com.browserbase.api.core.http.StreamResponse +import com.browserbase.api.models.sessions.SessionActParams +import com.browserbase.api.models.sessions.SessionActResponse +import com.browserbase.api.models.sessions.SessionEndParams +import com.browserbase.api.models.sessions.SessionEndResponse +import com.browserbase.api.models.sessions.SessionExecuteParams +import com.browserbase.api.models.sessions.SessionExecuteResponse +import com.browserbase.api.models.sessions.SessionExtractParams +import com.browserbase.api.models.sessions.SessionExtractResponse +import com.browserbase.api.models.sessions.SessionNavigateParams +import com.browserbase.api.models.sessions.SessionNavigateResponse +import com.browserbase.api.models.sessions.SessionObserveParams +import com.browserbase.api.models.sessions.SessionObserveResponse +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionReplayResponse +import com.browserbase.api.models.sessions.SessionStartParams +import com.browserbase.api.models.sessions.SessionStartResponse +import com.browserbase.api.models.sessions.StreamEvent +import com.google.errorprone.annotations.MustBeClosed +import java.util.function.Consumer + +interface SessionService { + + /** + * Returns a view of this service that provides access to raw HTTP responses for each method. + */ + fun withRawResponse(): WithRawResponse + + /** + * Returns a view of this service with the given option modifications applied. + * + * The original service is not modified. + */ + fun withOptions(modifier: Consumer): SessionService + + /** + * Executes a browser action using natural language instructions or a predefined Action object. + */ + fun act(id: String, params: SessionActParams): SessionActResponse = + act(id, params, RequestOptions.none()) + + /** @see act */ + fun act( + id: String, + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionActResponse = act(params.toBuilder().id(id).build(), requestOptions) + + /** @see act */ + fun act(params: SessionActParams): SessionActResponse = act(params, RequestOptions.none()) + + /** @see act */ + fun act( + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionActResponse + + /** + * Executes a browser action using natural language instructions or a predefined Action object. + */ + @MustBeClosed + fun actStreaming(id: String, params: SessionActParams): StreamResponse = + actStreaming(id, params, RequestOptions.none()) + + /** @see actStreaming */ + @MustBeClosed + fun actStreaming( + id: String, + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): StreamResponse = actStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see actStreaming */ + @MustBeClosed + fun actStreaming(params: SessionActParams): StreamResponse = + actStreaming(params, RequestOptions.none()) + + /** @see actStreaming */ + @MustBeClosed + fun actStreaming( + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): StreamResponse + + /** Terminates the browser session and releases all associated resources. */ + fun end(id: String): SessionEndResponse = end(id, SessionEndParams.none()) + + /** @see end */ + fun end( + id: String, + params: SessionEndParams = SessionEndParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionEndResponse = end(params.toBuilder().id(id).build(), requestOptions) + + /** @see end */ + fun end(id: String, params: SessionEndParams = SessionEndParams.none()): SessionEndResponse = + end(id, params, RequestOptions.none()) + + /** @see end */ + fun end( + params: SessionEndParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionEndResponse + + /** @see end */ + fun end(params: SessionEndParams): SessionEndResponse = end(params, RequestOptions.none()) + + /** @see end */ + fun end(id: String, requestOptions: RequestOptions): SessionEndResponse = + end(id, SessionEndParams.none(), requestOptions) + + /** Runs an autonomous AI agent that can perform complex multi-step browser tasks. */ + fun execute(id: String, params: SessionExecuteParams): SessionExecuteResponse = + execute(id, params, RequestOptions.none()) + + /** @see execute */ + fun execute( + id: String, + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionExecuteResponse = execute(params.toBuilder().id(id).build(), requestOptions) + + /** @see execute */ + fun execute(params: SessionExecuteParams): SessionExecuteResponse = + execute(params, RequestOptions.none()) + + /** @see execute */ + fun execute( + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionExecuteResponse + + /** Runs an autonomous AI agent that can perform complex multi-step browser tasks. */ + @MustBeClosed + fun executeStreaming(id: String, params: SessionExecuteParams): StreamResponse = + executeStreaming(id, params, RequestOptions.none()) + + /** @see executeStreaming */ + @MustBeClosed + fun executeStreaming( + id: String, + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): StreamResponse = + executeStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see executeStreaming */ + @MustBeClosed + fun executeStreaming(params: SessionExecuteParams): StreamResponse = + executeStreaming(params, RequestOptions.none()) + + /** @see executeStreaming */ + @MustBeClosed + fun executeStreaming( + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): StreamResponse + + /** Extracts structured data from the current page using AI-powered analysis. */ + fun extract(id: String): SessionExtractResponse = extract(id, SessionExtractParams.none()) + + /** @see extract */ + fun extract( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionExtractResponse = extract(params.toBuilder().id(id).build(), requestOptions) + + /** @see extract */ + fun extract( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + ): SessionExtractResponse = extract(id, params, RequestOptions.none()) + + /** @see extract */ + fun extract( + params: SessionExtractParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionExtractResponse + + /** @see extract */ + fun extract(params: SessionExtractParams): SessionExtractResponse = + extract(params, RequestOptions.none()) + + /** @see extract */ + fun extract(id: String, requestOptions: RequestOptions): SessionExtractResponse = + extract(id, SessionExtractParams.none(), requestOptions) + + /** Extracts structured data from the current page using AI-powered analysis. */ + @MustBeClosed + fun extractStreaming(id: String): StreamResponse = + extractStreaming(id, SessionExtractParams.none()) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): StreamResponse = + extractStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + ): StreamResponse = extractStreaming(id, params, RequestOptions.none()) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + params: SessionExtractParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): StreamResponse + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming(params: SessionExtractParams): StreamResponse = + extractStreaming(params, RequestOptions.none()) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming(id: String, requestOptions: RequestOptions): StreamResponse = + extractStreaming(id, SessionExtractParams.none(), requestOptions) + + /** Navigates the browser to the specified URL. */ + fun navigate(id: String, params: SessionNavigateParams): SessionNavigateResponse = + navigate(id, params, RequestOptions.none()) + + /** @see navigate */ + fun navigate( + id: String, + params: SessionNavigateParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionNavigateResponse = navigate(params.toBuilder().id(id).build(), requestOptions) + + /** @see navigate */ + fun navigate(params: SessionNavigateParams): SessionNavigateResponse = + navigate(params, RequestOptions.none()) + + /** @see navigate */ + fun navigate( + params: SessionNavigateParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionNavigateResponse + + /** + * Identifies and returns available actions on the current page that match the given + * instruction. + */ + fun observe(id: String): SessionObserveResponse = observe(id, SessionObserveParams.none()) + + /** @see observe */ + fun observe( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionObserveResponse = observe(params.toBuilder().id(id).build(), requestOptions) + + /** @see observe */ + fun observe( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + ): SessionObserveResponse = observe(id, params, RequestOptions.none()) + + /** @see observe */ + fun observe( + params: SessionObserveParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionObserveResponse + + /** @see observe */ + fun observe(params: SessionObserveParams): SessionObserveResponse = + observe(params, RequestOptions.none()) + + /** @see observe */ + fun observe(id: String, requestOptions: RequestOptions): SessionObserveResponse = + observe(id, SessionObserveParams.none(), requestOptions) + + /** + * Identifies and returns available actions on the current page that match the given + * instruction. + */ + @MustBeClosed + fun observeStreaming(id: String): StreamResponse = + observeStreaming(id, SessionObserveParams.none()) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): StreamResponse = + observeStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + ): StreamResponse = observeStreaming(id, params, RequestOptions.none()) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + params: SessionObserveParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): StreamResponse + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming(params: SessionObserveParams): StreamResponse = + observeStreaming(params, RequestOptions.none()) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming(id: String, requestOptions: RequestOptions): StreamResponse = + observeStreaming(id, SessionObserveParams.none(), requestOptions) + + /** Retrieves replay metrics for a session. */ + fun replay(id: String): SessionReplayResponse = replay(id, SessionReplayParams.none()) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionReplayResponse = replay(params.toBuilder().id(id).build(), requestOptions) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + ): SessionReplayResponse = replay(id, params, RequestOptions.none()) + + /** @see replay */ + fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionReplayResponse + + /** @see replay */ + fun replay(params: SessionReplayParams): SessionReplayResponse = + replay(params, RequestOptions.none()) + + /** @see replay */ + fun replay(id: String, requestOptions: RequestOptions): SessionReplayResponse = + replay(id, SessionReplayParams.none(), requestOptions) + + /** + * Creates a new browser session with the specified configuration. Returns a session ID used for + * all subsequent operations. + */ + fun start(params: SessionStartParams): SessionStartResponse = + start(params, RequestOptions.none()) + + /** @see start */ + fun start( + params: SessionStartParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionStartResponse + + /** A view of [SessionService] that provides access to raw HTTP responses for each method. */ + interface WithRawResponse { + + /** + * Returns a view of this service with the given option modifications applied. + * + * The original service is not modified. + */ + fun withOptions(modifier: Consumer): SessionService.WithRawResponse + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/act`, but is otherwise the same + * as [SessionService.act]. + */ + @MustBeClosed + fun act(id: String, params: SessionActParams): HttpResponseFor = + act(id, params, RequestOptions.none()) + + /** @see act */ + @MustBeClosed + fun act( + id: String, + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + act(params.toBuilder().id(id).build(), requestOptions) + + /** @see act */ + @MustBeClosed + fun act(params: SessionActParams): HttpResponseFor = + act(params, RequestOptions.none()) + + /** @see act */ + @MustBeClosed + fun act( + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/act`, but is otherwise the same + * as [SessionService.actStreaming]. + */ + @MustBeClosed + fun actStreaming( + id: String, + params: SessionActParams, + ): HttpResponseFor> = + actStreaming(id, params, RequestOptions.none()) + + /** @see actStreaming */ + @MustBeClosed + fun actStreaming( + id: String, + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor> = + actStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see actStreaming */ + @MustBeClosed + fun actStreaming(params: SessionActParams): HttpResponseFor> = + actStreaming(params, RequestOptions.none()) + + /** @see actStreaming */ + @MustBeClosed + fun actStreaming( + params: SessionActParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor> + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/end`, but is otherwise the same + * as [SessionService.end]. + */ + @MustBeClosed + fun end(id: String): HttpResponseFor = end(id, SessionEndParams.none()) + + /** @see end */ + @MustBeClosed + fun end( + id: String, + params: SessionEndParams = SessionEndParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + end(params.toBuilder().id(id).build(), requestOptions) + + /** @see end */ + @MustBeClosed + fun end( + id: String, + params: SessionEndParams = SessionEndParams.none(), + ): HttpResponseFor = end(id, params, RequestOptions.none()) + + /** @see end */ + @MustBeClosed + fun end( + params: SessionEndParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + + /** @see end */ + @MustBeClosed + fun end(params: SessionEndParams): HttpResponseFor = + end(params, RequestOptions.none()) + + /** @see end */ + @MustBeClosed + fun end(id: String, requestOptions: RequestOptions): HttpResponseFor = + end(id, SessionEndParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/agentExecute`, but is otherwise + * the same as [SessionService.execute]. + */ + @MustBeClosed + fun execute( + id: String, + params: SessionExecuteParams, + ): HttpResponseFor = execute(id, params, RequestOptions.none()) + + /** @see execute */ + @MustBeClosed + fun execute( + id: String, + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + execute(params.toBuilder().id(id).build(), requestOptions) + + /** @see execute */ + @MustBeClosed + fun execute(params: SessionExecuteParams): HttpResponseFor = + execute(params, RequestOptions.none()) + + /** @see execute */ + @MustBeClosed + fun execute( + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/agentExecute`, but is otherwise + * the same as [SessionService.executeStreaming]. + */ + @MustBeClosed + fun executeStreaming( + id: String, + params: SessionExecuteParams, + ): HttpResponseFor> = + executeStreaming(id, params, RequestOptions.none()) + + /** @see executeStreaming */ + @MustBeClosed + fun executeStreaming( + id: String, + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor> = + executeStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see executeStreaming */ + @MustBeClosed + fun executeStreaming( + params: SessionExecuteParams + ): HttpResponseFor> = + executeStreaming(params, RequestOptions.none()) + + /** @see executeStreaming */ + @MustBeClosed + fun executeStreaming( + params: SessionExecuteParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor> + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/extract`, but is otherwise the + * same as [SessionService.extract]. + */ + @MustBeClosed + fun extract(id: String): HttpResponseFor = + extract(id, SessionExtractParams.none()) + + /** @see extract */ + @MustBeClosed + fun extract( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + extract(params.toBuilder().id(id).build(), requestOptions) + + /** @see extract */ + @MustBeClosed + fun extract( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + ): HttpResponseFor = extract(id, params, RequestOptions.none()) + + /** @see extract */ + @MustBeClosed + fun extract( + params: SessionExtractParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + + /** @see extract */ + @MustBeClosed + fun extract(params: SessionExtractParams): HttpResponseFor = + extract(params, RequestOptions.none()) + + /** @see extract */ + @MustBeClosed + fun extract( + id: String, + requestOptions: RequestOptions, + ): HttpResponseFor = + extract(id, SessionExtractParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/extract`, but is otherwise the + * same as [SessionService.extractStreaming]. + */ + @MustBeClosed + fun extractStreaming(id: String): HttpResponseFor> = + extractStreaming(id, SessionExtractParams.none()) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor> = + extractStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + id: String, + params: SessionExtractParams = SessionExtractParams.none(), + ): HttpResponseFor> = + extractStreaming(id, params, RequestOptions.none()) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + params: SessionExtractParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor> + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + params: SessionExtractParams + ): HttpResponseFor> = + extractStreaming(params, RequestOptions.none()) + + /** @see extractStreaming */ + @MustBeClosed + fun extractStreaming( + id: String, + requestOptions: RequestOptions, + ): HttpResponseFor> = + extractStreaming(id, SessionExtractParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/navigate`, but is otherwise the + * same as [SessionService.navigate]. + */ + @MustBeClosed + fun navigate( + id: String, + params: SessionNavigateParams, + ): HttpResponseFor = navigate(id, params, RequestOptions.none()) + + /** @see navigate */ + @MustBeClosed + fun navigate( + id: String, + params: SessionNavigateParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + navigate(params.toBuilder().id(id).build(), requestOptions) + + /** @see navigate */ + @MustBeClosed + fun navigate(params: SessionNavigateParams): HttpResponseFor = + navigate(params, RequestOptions.none()) + + /** @see navigate */ + @MustBeClosed + fun navigate( + params: SessionNavigateParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/observe`, but is otherwise the + * same as [SessionService.observe]. + */ + @MustBeClosed + fun observe(id: String): HttpResponseFor = + observe(id, SessionObserveParams.none()) + + /** @see observe */ + @MustBeClosed + fun observe( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + observe(params.toBuilder().id(id).build(), requestOptions) + + /** @see observe */ + @MustBeClosed + fun observe( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + ): HttpResponseFor = observe(id, params, RequestOptions.none()) + + /** @see observe */ + @MustBeClosed + fun observe( + params: SessionObserveParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + + /** @see observe */ + @MustBeClosed + fun observe(params: SessionObserveParams): HttpResponseFor = + observe(params, RequestOptions.none()) + + /** @see observe */ + @MustBeClosed + fun observe( + id: String, + requestOptions: RequestOptions, + ): HttpResponseFor = + observe(id, SessionObserveParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/{id}/observe`, but is otherwise the + * same as [SessionService.observeStreaming]. + */ + @MustBeClosed + fun observeStreaming(id: String): HttpResponseFor> = + observeStreaming(id, SessionObserveParams.none()) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor> = + observeStreaming(params.toBuilder().id(id).build(), requestOptions) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + id: String, + params: SessionObserveParams = SessionObserveParams.none(), + ): HttpResponseFor> = + observeStreaming(id, params, RequestOptions.none()) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + params: SessionObserveParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor> + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + params: SessionObserveParams + ): HttpResponseFor> = + observeStreaming(params, RequestOptions.none()) + + /** @see observeStreaming */ + @MustBeClosed + fun observeStreaming( + id: String, + requestOptions: RequestOptions, + ): HttpResponseFor> = + observeStreaming(id, SessionObserveParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `get /v1/sessions/{id}/replay`, but is otherwise the same + * as [SessionService.replay]. + */ + @MustBeClosed + fun replay(id: String): HttpResponseFor = + replay(id, SessionReplayParams.none()) + + /** @see replay */ + @MustBeClosed + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + replay(params.toBuilder().id(id).build(), requestOptions) + + /** @see replay */ + @MustBeClosed + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + ): HttpResponseFor = replay(id, params, RequestOptions.none()) + + /** @see replay */ + @MustBeClosed + fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + + /** @see replay */ + @MustBeClosed + fun replay(params: SessionReplayParams): HttpResponseFor = + replay(params, RequestOptions.none()) + + /** @see replay */ + @MustBeClosed + fun replay( + id: String, + requestOptions: RequestOptions, + ): HttpResponseFor = + replay(id, SessionReplayParams.none(), requestOptions) + + /** + * Returns a raw HTTP response for `post /v1/sessions/start`, but is otherwise the same as + * [SessionService.start]. + */ + @MustBeClosed + fun start(params: SessionStartParams): HttpResponseFor = + start(params, RequestOptions.none()) + + /** @see start */ + @MustBeClosed + fun start( + params: SessionStartParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + } +} 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 new file mode 100644 index 0000000..8367bf6 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionServiceImpl.kt @@ -0,0 +1,563 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.services.blocking + +import com.browserbase.api.core.ClientOptions +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.RequestOptions +import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.handlers.errorBodyHandler +import com.browserbase.api.core.handlers.errorHandler +import com.browserbase.api.core.handlers.jsonHandler +import com.browserbase.api.core.handlers.mapJson +import com.browserbase.api.core.handlers.sseHandler +import com.browserbase.api.core.http.HttpMethod +import com.browserbase.api.core.http.HttpRequest +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.core.http.HttpResponse.Handler +import com.browserbase.api.core.http.HttpResponseFor +import com.browserbase.api.core.http.StreamResponse +import com.browserbase.api.core.http.json +import com.browserbase.api.core.http.map +import com.browserbase.api.core.http.parseable +import com.browserbase.api.core.prepare +import com.browserbase.api.models.sessions.SessionActParams +import com.browserbase.api.models.sessions.SessionActResponse +import com.browserbase.api.models.sessions.SessionEndParams +import com.browserbase.api.models.sessions.SessionEndResponse +import com.browserbase.api.models.sessions.SessionExecuteParams +import com.browserbase.api.models.sessions.SessionExecuteResponse +import com.browserbase.api.models.sessions.SessionExtractParams +import com.browserbase.api.models.sessions.SessionExtractResponse +import com.browserbase.api.models.sessions.SessionNavigateParams +import com.browserbase.api.models.sessions.SessionNavigateResponse +import com.browserbase.api.models.sessions.SessionObserveParams +import com.browserbase.api.models.sessions.SessionObserveResponse +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionReplayResponse +import com.browserbase.api.models.sessions.SessionStartParams +import com.browserbase.api.models.sessions.SessionStartResponse +import com.browserbase.api.models.sessions.StreamEvent +import java.util.function.Consumer +import kotlin.jvm.optionals.getOrNull + +class SessionServiceImpl internal constructor(private val clientOptions: ClientOptions) : + SessionService { + + private val withRawResponse: SessionService.WithRawResponse by lazy { + WithRawResponseImpl(clientOptions) + } + + override fun withRawResponse(): SessionService.WithRawResponse = withRawResponse + + override fun withOptions(modifier: Consumer): SessionService = + SessionServiceImpl(clientOptions.toBuilder().apply(modifier::accept).build()) + + override fun act(params: SessionActParams, requestOptions: RequestOptions): SessionActResponse = + // post /v1/sessions/{id}/act + withRawResponse().act(params, requestOptions).parse() + + override fun actStreaming( + params: SessionActParams, + requestOptions: RequestOptions, + ): StreamResponse = + // post /v1/sessions/{id}/act + withRawResponse().actStreaming(params, requestOptions).parse() + + override fun end(params: SessionEndParams, requestOptions: RequestOptions): SessionEndResponse = + // post /v1/sessions/{id}/end + withRawResponse().end(params, requestOptions).parse() + + override fun execute( + params: SessionExecuteParams, + requestOptions: RequestOptions, + ): SessionExecuteResponse = + // post /v1/sessions/{id}/agentExecute + withRawResponse().execute(params, requestOptions).parse() + + override fun executeStreaming( + params: SessionExecuteParams, + requestOptions: RequestOptions, + ): StreamResponse = + // post /v1/sessions/{id}/agentExecute + withRawResponse().executeStreaming(params, requestOptions).parse() + + override fun extract( + params: SessionExtractParams, + requestOptions: RequestOptions, + ): SessionExtractResponse = + // post /v1/sessions/{id}/extract + withRawResponse().extract(params, requestOptions).parse() + + override fun extractStreaming( + params: SessionExtractParams, + requestOptions: RequestOptions, + ): StreamResponse = + // post /v1/sessions/{id}/extract + withRawResponse().extractStreaming(params, requestOptions).parse() + + override fun navigate( + params: SessionNavigateParams, + requestOptions: RequestOptions, + ): SessionNavigateResponse = + // post /v1/sessions/{id}/navigate + withRawResponse().navigate(params, requestOptions).parse() + + override fun observe( + params: SessionObserveParams, + requestOptions: RequestOptions, + ): SessionObserveResponse = + // post /v1/sessions/{id}/observe + withRawResponse().observe(params, requestOptions).parse() + + override fun observeStreaming( + params: SessionObserveParams, + requestOptions: RequestOptions, + ): StreamResponse = + // post /v1/sessions/{id}/observe + withRawResponse().observeStreaming(params, requestOptions).parse() + + override fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions, + ): SessionReplayResponse = + // get /v1/sessions/{id}/replay + withRawResponse().replay(params, requestOptions).parse() + + override fun start( + params: SessionStartParams, + requestOptions: RequestOptions, + ): SessionStartResponse = + // post /v1/sessions/start + withRawResponse().start(params, requestOptions).parse() + + class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) : + SessionService.WithRawResponse { + + private val errorHandler: Handler = + errorHandler(errorBodyHandler(clientOptions.jsonMapper)) + + override fun withOptions( + modifier: Consumer + ): SessionService.WithRawResponse = + SessionServiceImpl.WithRawResponseImpl( + clientOptions.toBuilder().apply(modifier::accept).build() + ) + + private val actHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun act( + params: SessionActParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "act") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { actHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + + private val actStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper).mapJson() + + override fun actStreaming( + params: SessionActParams, + requestOptions: RequestOptions, + ): HttpResponseFor> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "act") + .putHeader("Accept", "text/event-stream") + .body( + json( + clientOptions.jsonMapper, + params + ._body() + .toBuilder() + .putAdditionalProperty("streamResponse", JsonValue.from(true)) + .build(), + ) + ) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .let { actStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation!!) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + } + + private val endHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun end( + params: SessionEndParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "end") + .apply { params._body().ifPresent { body(json(clientOptions.jsonMapper, it)) } } + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { endHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + + private val executeHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun execute( + params: SessionExecuteParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "agentExecute") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { executeHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + + private val executeStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper).mapJson() + + override fun executeStreaming( + params: SessionExecuteParams, + requestOptions: RequestOptions, + ): HttpResponseFor> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "agentExecute") + .putHeader("Accept", "text/event-stream") + .body( + json( + clientOptions.jsonMapper, + params + ._body() + .toBuilder() + .putAdditionalProperty("streamResponse", JsonValue.from(true)) + .build(), + ) + ) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .let { executeStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation!!) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + } + + private val extractHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun extract( + params: SessionExtractParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "extract") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { extractHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + + private val extractStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper).mapJson() + + override fun extractStreaming( + params: SessionExtractParams, + requestOptions: RequestOptions, + ): HttpResponseFor> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "extract") + .putHeader("Accept", "text/event-stream") + .body( + json( + clientOptions.jsonMapper, + params + ._body() + .toBuilder() + .putAdditionalProperty("streamResponse", JsonValue.from(true)) + .build(), + ) + ) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .let { extractStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation!!) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + } + + private val navigateHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun navigate( + params: SessionNavigateParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "navigate") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { navigateHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + + private val observeHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun observe( + params: SessionObserveParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "observe") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { observeHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + + private val observeStreamingHandler: Handler> = + sseHandler(clientOptions.jsonMapper).mapJson() + + override fun observeStreaming( + params: SessionObserveParams, + requestOptions: RequestOptions, + ): HttpResponseFor> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "observe") + .putHeader("Accept", "text/event-stream") + .body( + json( + clientOptions.jsonMapper, + params + ._body() + .toBuilder() + .putAdditionalProperty("streamResponse", JsonValue.from(true)) + .build(), + ) + ) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .let { observeStreamingHandler.handle(it) } + .let { streamResponse -> + if (requestOptions.responseValidation!!) { + streamResponse.map { it.validate() } + } else { + streamResponse + } + } + } + } + + private val replayHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "replay") + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { replayHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + + private val startHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun start( + params: SessionStartParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + val request = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", "start") + .body(json(clientOptions.jsonMapper, params._body())) + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { startHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } +} diff --git a/stagehand-java-core/src/main/resources/META-INF/proguard/stagehand-java-core.pro b/stagehand-java-core/src/main/resources/META-INF/proguard/stagehand-java-core.pro new file mode 100644 index 0000000..474257e --- /dev/null +++ b/stagehand-java-core/src/main/resources/META-INF/proguard/stagehand-java-core.pro @@ -0,0 +1,32 @@ +# Jackson uses reflection and depends heavily on runtime attributes. +-keepattributes Exceptions,InnerClasses,Signature,Deprecated,*Annotation* + +# Jackson uses Kotlin reflection utilities, which themselves use reflection to access things. +-keep class kotlin.reflect.** { *; } +-keep class kotlin.Metadata { *; } + +# Jackson uses reflection to access enum members (e.g. via `java.lang.Class.getEnumConstants()`). +-keepclassmembers class com.fasterxml.jackson.** extends java.lang.Enum { + ; + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# Jackson uses reflection to access annotation members. +-keepclassmembers @interface com.fasterxml.jackson.annotation.** { + *; +} + +# Jackson uses reified type information to serialize and deserialize our classes (via `TypeReference`). +-keep class com.fasterxml.jackson.core.type.TypeReference { *; } +-keep class * extends com.fasterxml.jackson.core.type.TypeReference { *; } + +# Jackson uses reflection to access our class serializers and deserializers. +-keep @com.fasterxml.jackson.databind.annotation.JsonSerialize class com.browserbase.api.** { *; } +-keep @com.fasterxml.jackson.databind.annotation.JsonDeserialize class com.browserbase.api.** { *; } + +# Jackson uses reflection to serialize and deserialize our classes based on their constructors and annotated members. +-keepclassmembers class com.browserbase.api.** { + (...); + @com.fasterxml.jackson.annotation.* *; +} \ No newline at end of file diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ClientOptionsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ClientOptionsTest.kt new file mode 100644 index 0000000..1bd88a6 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ClientOptionsTest.kt @@ -0,0 +1,70 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core + +import com.browserbase.api.core.http.HttpClient +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@ExtendWith(MockitoExtension::class) +internal class ClientOptionsTest { + + private val httpClient = mock() + + @Test + fun putHeader_canOverwriteDefaultHeader() { + val clientOptions = + ClientOptions.builder() + .httpClient(httpClient) + .putHeader("User-Agent", "My User Agent") + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + + assertThat(clientOptions.headers.values("User-Agent")).containsExactly("My User Agent") + } + + @Test + fun toBuilder_bbApiKeyAuthCanBeUpdated() { + var clientOptions = + ClientOptions.builder() + .httpClient(httpClient) + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + + clientOptions = + clientOptions.toBuilder().browserbaseApiKey("another My Browserbase API Key").build() + + assertThat(clientOptions.headers.values("x-bb-api-key")) + .containsExactly("another My Browserbase API Key") + } + + @Test + fun toBuilder_whenOriginalClientOptionsGarbageCollected_doesNotCloseOriginalClient() { + var clientOptions = + ClientOptions.builder() + .httpClient(httpClient) + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + verify(httpClient, never()).close() + + // Overwrite the `clientOptions` variable so that the original `ClientOptions` is GC'd. + clientOptions = clientOptions.toBuilder().build() + System.gc() + Thread.sleep(100) + + verify(httpClient, never()).close() + // This exists so that `clientOptions` is still reachable. + assertThat(clientOptions).isEqualTo(clientOptions) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ObjectMappersTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ObjectMappersTest.kt new file mode 100644 index 0000000..62c4108 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ObjectMappersTest.kt @@ -0,0 +1,117 @@ +package com.browserbase.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.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.junitpioneer.jupiter.cartesian.CartesianTest + +internal class ObjectMappersTest { + + internal class ClassWithBooleanFieldPrefixedWithIs(private val isActive: JsonField) { + + @JsonProperty("is_active") @ExcludeMissing fun _isActive() = isActive + } + + @Test + fun write_whenFieldPrefixedWithIs_keepsPrefix() { + val value = ClassWithBooleanFieldPrefixedWithIs(JsonField.of(true)) + + val json = jsonMapper().writeValueAsString(value) + + assertThat(json).isEqualTo("{\"is_active\":true}") + } + + internal class Class(@get:JsonProperty("field") @JsonProperty("field") val field: String) + + enum class ShapeTestCase(val value: Any, val kClass: KClass<*>) { + STRING("Hello World!", String::class), + BOOLEAN(true, Boolean::class), + FLOAT(3.14F, Float::class), + DOUBLE(3.14, Double::class), + INTEGER(42, Int::class), + LONG(42L, Long::class), + MAP(mapOf("property" to "value"), Map::class), + CLASS(Class("Hello World!"), Class::class), + LIST(listOf(1, 2, 3), List::class); + + companion object { + val VALID_CONVERSIONS = + listOf( + FLOAT to DOUBLE, + DOUBLE to FLOAT, + INTEGER to FLOAT, + INTEGER to DOUBLE, + INTEGER to LONG, + LONG to FLOAT, + LONG to DOUBLE, + LONG to INTEGER, + CLASS to MAP, + ) + } + } + + @CartesianTest + fun read(@CartesianTest.Enum shape1: ShapeTestCase, @CartesianTest.Enum shape2: ShapeTestCase) { + val jsonMapper = jsonMapper() + val json = jsonMapper.writeValueAsString(shape1.value) + + val e = catchThrowable { jsonMapper.readValue(json, shape2.kClass.java) } + + if (shape1 == shape2 || shape1 to shape2 in ShapeTestCase.VALID_CONVERSIONS) { + assertThat(e).isNull() + } else { + assertThat(e).isInstanceOf(MismatchedInputException::class.java) + } + } + + 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 + @EnumSource + fun readOffsetDateTime_lenient(testCase: LenientOffsetDateTimeTestCase) { + val jsonMapper = jsonMapper() + val json = jsonMapper.writeValueAsString(testCase.string) + + val offsetDateTime = jsonMapper().readValue(json) + + assertThat(offsetDateTime).isEqualTo(testCase.expectedOffsetDateTime) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/PhantomReachableTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/PhantomReachableTest.kt new file mode 100644 index 0000000..b1d98ae --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/PhantomReachableTest.kt @@ -0,0 +1,27 @@ +package com.browserbase.api.core + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class PhantomReachableTest { + + @Test + fun closeWhenPhantomReachable_whenObservedIsGarbageCollected_closesCloseable() { + var closed = false + val closeable = AutoCloseable { closed = true } + + closeWhenPhantomReachable( + // Pass an inline object for the object to observe so that it becomes immediately + // unreachable. + Any(), + closeable, + ) + + assertThat(closed).isFalse() + + System.gc() + Thread.sleep(100) + + assertThat(closed).isTrue() + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/UtilsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/UtilsTest.kt new file mode 100644 index 0000000..163e1a2 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/UtilsTest.kt @@ -0,0 +1,33 @@ +package com.browserbase.api.core + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class UtilsTest { + @Test + fun contentDeepEquals() { + assertThat(42 contentEquals 42).isTrue() + assertThat(42 contentEquals "Hello World!").isFalse() + assertThat(byteArrayOf(1, 2, 3) contentEquals byteArrayOf(1, 2, 3)).isTrue() + assertThat(byteArrayOf(1, 2, 3) contentEquals byteArrayOf(1, 2, 4)).isFalse() + assertThat( + arrayOf(byteArrayOf(1, 2), byteArrayOf(3)) contentEquals + arrayOf(byteArrayOf(1, 2), byteArrayOf(3)) + ) + .isTrue() + assertThat( + arrayOf(byteArrayOf(1, 2), byteArrayOf(3)) contentEquals + arrayOf(byteArrayOf(1), byteArrayOf(2, 3)) + ) + .isFalse() + } + + @Test + fun contentToString() { + assertThat((42).contentToString()).isEqualTo("42") + assertThat("Hello World!".contentToString()).isEqualTo("Hello World!") + assertThat(byteArrayOf(1, 2, 3).contentToString()).isEqualTo("[1, 2, 3]") + assertThat(arrayOf(byteArrayOf(1, 2), byteArrayOf(3)).contentToString()) + .isEqualTo("[[1, 2], [3]]") + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ValuesTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ValuesTest.kt new file mode 100644 index 0000000..d098377 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ValuesTest.kt @@ -0,0 +1,144 @@ +package com.browserbase.api.core + +import java.util.Optional +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +internal class ValuesTest { + companion object { + private val NON_JSON = Any() + } + + enum class TestCase( + val value: JsonField<*>, + val expectedIsMissing: Boolean = false, + val expectedIsNull: Boolean = false, + val expectedAsKnown: Optional<*> = Optional.empty(), + val expectedAsBoolean: Optional = Optional.empty(), + val expectedAsNumber: Optional = Optional.empty(), + val expectedAsString: Optional = Optional.empty(), + val expectedAsArray: Optional> = Optional.empty(), + val expectedAsObject: Optional> = Optional.empty(), + ) { + MISSING(JsonMissing.of(), expectedIsMissing = true), + NULL(JsonNull.of(), expectedIsNull = true), + KNOWN(KnownValue.of(NON_JSON), expectedAsKnown = Optional.of(NON_JSON)), + KNOWN_BOOLEAN( + KnownValue.of(true), + expectedAsKnown = Optional.of(true), + expectedAsBoolean = Optional.of(true), + ), + BOOLEAN(JsonBoolean.of(true), expectedAsBoolean = Optional.of(true)), + KNOWN_NUMBER( + KnownValue.of(42), + expectedAsKnown = Optional.of(42), + expectedAsNumber = Optional.of(42), + ), + NUMBER(JsonNumber.of(42), expectedAsNumber = Optional.of(42)), + KNOWN_STRING( + KnownValue.of("hello"), + expectedAsKnown = Optional.of("hello"), + expectedAsString = Optional.of("hello"), + ), + STRING(JsonString.of("hello"), expectedAsString = Optional.of("hello")), + KNOWN_ARRAY_NOT_ALL_JSON( + KnownValue.of(listOf("a", "b", NON_JSON)), + expectedAsKnown = Optional.of(listOf("a", "b", NON_JSON)), + ), + KNOWN_ARRAY( + KnownValue.of(listOf("a", "b", "c")), + expectedAsKnown = Optional.of(listOf("a", "b", "c")), + expectedAsArray = + Optional.of(listOf(JsonString.of("a"), JsonString.of("b"), JsonString.of("c"))), + ), + ARRAY( + JsonArray.of(listOf(JsonString.of("a"), JsonString.of("b"), JsonString.of("c"))), + expectedAsArray = + Optional.of(listOf(JsonString.of("a"), JsonString.of("b"), JsonString.of("c"))), + ), + KNOWN_OBJECT_NOT_ALL_STRING_KEYS( + KnownValue.of(mapOf("a" to "b", 42 to "c")), + expectedAsKnown = Optional.of(mapOf("a" to "b", 42 to "c")), + ), + KNOWN_OBJECT_NOT_ALL_JSON( + KnownValue.of(mapOf("a" to "b", "b" to NON_JSON)), + expectedAsKnown = Optional.of(mapOf("a" to "b", "b" to NON_JSON)), + ), + KNOWN_OBJECT( + KnownValue.of(mapOf("a" to "b", "b" to "c")), + expectedAsKnown = Optional.of(mapOf("a" to "b", "b" to "c")), + expectedAsObject = + Optional.of(mapOf("a" to JsonString.of("b"), "b" to JsonString.of("c"))), + ), + OBJECT( + JsonObject.of(mapOf("a" to JsonString.of("b"), "b" to JsonString.of("c"))), + expectedAsObject = + Optional.of(mapOf("a" to JsonString.of("b"), "b" to JsonString.of("c"))), + ), + } + + @ParameterizedTest + @EnumSource + fun isMissing(testCase: TestCase) { + val isMissing = testCase.value.isMissing() + + assertThat(isMissing).isEqualTo(testCase.expectedIsMissing) + } + + @ParameterizedTest + @EnumSource + fun isNull(testCase: TestCase) { + val isNull = testCase.value.isNull() + + assertThat(isNull).isEqualTo(testCase.expectedIsNull) + } + + @ParameterizedTest + @EnumSource + fun asKnown(testCase: TestCase) { + val known = testCase.value.asKnown() + + assertThat(known).isEqualTo(testCase.expectedAsKnown) + } + + @ParameterizedTest + @EnumSource + fun asBoolean(testCase: TestCase) { + val boolean = testCase.value.asBoolean() + + assertThat(boolean).isEqualTo(testCase.expectedAsBoolean) + } + + @ParameterizedTest + @EnumSource + fun asNumber(testCase: TestCase) { + val number = testCase.value.asNumber() + + assertThat(number).isEqualTo(testCase.expectedAsNumber) + } + + @ParameterizedTest + @EnumSource + fun asString(testCase: TestCase) { + val string = testCase.value.asString() + + assertThat(string).isEqualTo(testCase.expectedAsString) + } + + @ParameterizedTest + @EnumSource + fun asArray(testCase: TestCase) { + val array = testCase.value.asArray() + + assertThat(array).isEqualTo(testCase.expectedAsArray) + } + + @ParameterizedTest + @EnumSource + fun asObject(testCase: TestCase) { + val obj = testCase.value.asObject() + + assertThat(obj).isEqualTo(testCase.expectedAsObject) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/SseHandlerTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/SseHandlerTest.kt new file mode 100644 index 0000000..2e4dfb0 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/SseHandlerTest.kt @@ -0,0 +1,177 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.handlers + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.core.http.SseMessage +import com.browserbase.api.core.jsonMapper +import com.browserbase.api.errors.SseException +import java.io.InputStream +import java.util.stream.Collectors.toList +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.catchThrowable +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +internal class SseHandlerTest { + + enum class TestCase( + internal val body: String, + internal val expectedMessages: List? = null, + internal val expectedException: Exception? = null, + ) { + EVENT_AND_DATA( + buildString { + append("event: starting\n") + append("data: {\"foo\":true}\n") + append("\n") + }, + listOf(sseMessageBuilder().event("starting").data("{\"foo\":true}").build()), + ), + EVENT_MISSING_DATA( + buildString { + append("event: starting\n") + append("\n") + }, + listOf(sseMessageBuilder().event("starting").build()), + ), + MULTIPLE_EVENTS_AND_DATA( + buildString { + append("event: starting\n") + append("data: {\"foo\":true}\n") + append("\n") + append("event: connected\n") + append("data: {\"bar\":false}\n") + append("\n") + }, + listOf( + sseMessageBuilder().event("starting").data("{\"foo\":true}").build(), + sseMessageBuilder().event("connected").data("{\"bar\":false}").build(), + ), + ), + MULTIPLE_EVENTS_MISSING_DATA( + buildString { + append("event: starting\n") + append("\n") + append("event: connected\n") + append("\n") + }, + listOf( + sseMessageBuilder().event("starting").build(), + sseMessageBuilder().event("connected").build(), + ), + ), + DATA_JSON_ESCAPED_DOUBLE_NEW_LINE( + buildString { + append("event: starting\n") + append("data: {\n") + append("data: \"foo\":\n") + append("data: true}\n") + append("\n\n") + }, + listOf(sseMessageBuilder().event("starting").data("{\n\"foo\":\ntrue}").build()), + ), + MULTIPLE_DATA_LINES( + buildString { + append("event: starting\n") + append("data: {\n") + append("data: \"foo\":\n") + append("data: true}\n") + append("\n\n") + }, + listOf(sseMessageBuilder().event("starting").data("{\n\"foo\":\ntrue}").build()), + ), + SPECIAL_NEW_LINE_CHARACTER( + buildString { + append("event: starting\n") + append("data: {\"content\":\" culpa\"}\n") + append("\n") + append("event: connected\n") + append("data: {\"content\":\" \u2028\"}\n") + append("\n") + append("event: starting\n") + append("data: {\"content\":\"foo\"}\n") + append("\n") + }, + listOf( + sseMessageBuilder().event("starting").data("{\"content\":\" culpa\"}").build(), + sseMessageBuilder().event("connected").data("{\"content\":\" \u2028\"}").build(), + sseMessageBuilder().event("starting").data("{\"content\":\"foo\"}").build(), + ), + ), + MULTI_BYTE_CHARACTER( + buildString { + append("event: starting\n") + append("data: {\"content\":\"\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0438\"}\n") + append("\n") + }, + listOf(sseMessageBuilder().event("starting").data("{\"content\":\"известни\"}").build()), + ), + ERROR_EVENT( + buildString { + append("event: error\n") + append("data: {\"errorProperty\":\"42\"}\n") + append("\n") + }, + expectedException = + SseException.builder() + .statusCode(0) + .headers(Headers.builder().build()) + .body(JsonValue.from(mapOf("errorProperty" to "42"))) + .build(), + ), + } + + @ParameterizedTest + @EnumSource + fun handle(testCase: TestCase) { + val response = httpResponse(testCase.body) + var messages: List? = null + var exception: Exception? = null + + try { + messages = + sseHandler(jsonMapper()).handle(response).use { it.stream().collect(toList()) } + } catch (e: Exception) { + exception = e + } + + if (testCase.expectedMessages != null) { + assertThat(messages).containsExactlyElementsOf(testCase.expectedMessages) + } + if (testCase.expectedException != null) { + assertThat(exception).isInstanceOf(testCase.expectedException.javaClass) + assertThat(exception).hasMessage(testCase.expectedException.message) + } + } + + @Test + fun cannotReuseStream() { + val response = httpResponse("body") + val streamResponse = sseHandler(jsonMapper()).handle(response) + + val throwable = + streamResponse.use { + it.stream().collect(toList()) + catchThrowable { it.stream().collect(toList()) } + } + + assertThat(throwable).isInstanceOf(IllegalStateException::class.java) + } +} + +private fun httpResponse(body: String): HttpResponse = + object : HttpResponse { + override fun statusCode(): Int = 0 + + override fun headers(): Headers = Headers.builder().build() + + override fun body(): InputStream = body.toByteArray().inputStream() + + override fun close() {} + } + +private fun sseMessageBuilder() = SseMessage.builder().jsonMapper(jsonMapper()) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/StreamHandlerTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/StreamHandlerTest.kt new file mode 100644 index 0000000..7da5378 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/StreamHandlerTest.kt @@ -0,0 +1,94 @@ +package com.browserbase.api.core.handlers + +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.HttpResponse +import com.browserbase.api.errors.StagehandIoException +import java.io.IOException +import java.io.InputStream +import kotlin.streams.asSequence +import kotlin.test.Test +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.assertThrows + +internal class StreamHandlerTest { + + @Test + fun streamHandler_splitsStreamOnNewlines() { + val handler = streamHandler { _, lines -> yieldAll(lines) } + val streamResponse = handler.handle(httpResponse("a\nbb\nccc\ndddd".byteInputStream())) + + val lines = streamResponse.stream().asSequence().toList() + + assertThat(lines).containsExactly("a", "bb", "ccc", "dddd") + } + + @Test + fun streamHandler_whenClosedEarly_stopsYielding() { + val handler = streamHandler { _, lines -> yieldAll(lines) } + val streamResponse = handler.handle(httpResponse("a\nbb\nccc\ndddd".byteInputStream())) + + val lines = + streamResponse + .stream() + .asSequence() + .onEach { + if (it == "bb") { + streamResponse.close() + } + } + .toList() + + assertThat(lines).containsExactly("a", "bb") + } + + @Test + fun streamHandler_whenReaderThrowsIOException_wrapsException() { + val handler = streamHandler { _, lines -> lines.forEach {} } + val streamResponse = handler.handle(httpResponse("a\nb\nc\n".byteInputStream().throwing())) + + val e = assertThrows { streamResponse.stream().forEach {} } + assertThat(e).hasMessage("Stream failed") + assertThat(e).hasCauseInstanceOf(IOException::class.java) + } + + @Test + fun streamHandler_whenBlockThrowsIOException_doesNotWrapException() { + val ioException = IOException("BOOM!") + val handler = + streamHandler { _, lines -> + lines.forEachIndexed { index, _ -> + if (index == 2) { + throw ioException + } + } + } + val streamResponse = handler.handle(httpResponse("a\nb\nc\n".byteInputStream())) + + val e = assertThrows { streamResponse.stream().forEach {} } + assertThat(e).isSameAs(ioException) + } + + private fun httpResponse(body: InputStream): HttpResponse = + object : HttpResponse { + + override fun statusCode(): Int = 0 + + override fun headers(): Headers = Headers.builder().build() + + override fun body(): InputStream = body + + override fun close() {} + } + + private fun InputStream.throwing(): InputStream = + object : InputStream() { + + override fun read(): Int { + val byte = this@throwing.read() + if (byte == -1) { + throw IOException("BOOM!") + } + return byte + } + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/AsyncStreamResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/AsyncStreamResponseTest.kt new file mode 100644 index 0000000..c5d8e71 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/AsyncStreamResponseTest.kt @@ -0,0 +1,268 @@ +package com.browserbase.api.core.http + +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.stream.Stream +import kotlin.streams.asStream +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.api.extension.ExtendWith +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.* + +@ExtendWith(MockitoExtension::class) +internal class AsyncStreamResponseTest { + + companion object { + private val ERROR = RuntimeException("ERROR!") + } + + private val streamResponse = + spy> { + doReturn(Stream.of("chunk1", "chunk2", "chunk3")).whenever(it).stream() + } + private val erroringStreamResponse = + spy> { + doReturn( + sequence { + yield("chunk1") + yield("chunk2") + throw ERROR + } + .asStream() + ) + .whenever(it) + .stream() + } + private val executor = + spy { + doAnswer { invocation -> invocation.getArgument(0).run() } + .whenever(it) + .execute(any()) + } + private val handler = mock>() + + @Test + fun subscribe_whenAlreadySubscribed_throws() { + val asyncStreamResponse = CompletableFuture>().toAsync(executor) + asyncStreamResponse.subscribe {} + + val throwable = catchThrowable { asyncStreamResponse.subscribe {} } + + assertThat(throwable).isInstanceOf(IllegalStateException::class.java) + assertThat(throwable).hasMessage("Cannot subscribe more than once") + verify(executor, never()).execute(any()) + } + + @Test + fun subscribe_whenClosed_throws() { + val asyncStreamResponse = CompletableFuture>().toAsync(executor) + asyncStreamResponse.close() + + val throwable = catchThrowable { asyncStreamResponse.subscribe {} } + + assertThat(throwable).isInstanceOf(IllegalStateException::class.java) + assertThat(throwable).hasMessage("Cannot subscribe after the response is closed") + verify(executor, never()).execute(any()) + } + + @Test + fun subscribe_whenFutureCompletesAfterClose_doesNothing() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + asyncStreamResponse.close() + + future.complete(streamResponse) + + verify(handler, never()).onNext(any()) + verify(handler, never()).onComplete(any()) + verify(executor, times(1)).execute(any()) + } + + @Test + fun subscribe_whenFutureErrors_callsOnComplete() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + + future.completeExceptionally(ERROR) + + verify(handler, never()).onNext(any()) + verify(handler, times(1)).onComplete(Optional.of(ERROR)) + verify(executor, times(1)).execute(any()) + } + + @Test + fun subscribe_whenFutureCompletes_runsHandler() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + + future.complete(streamResponse) + + inOrder(handler, streamResponse) { + verify(handler, times(1)).onNext("chunk1") + verify(handler, times(1)).onNext("chunk2") + verify(handler, times(1)).onNext("chunk3") + verify(handler, times(1)).onComplete(Optional.empty()) + verify(streamResponse, times(1)).close() + } + verify(executor, times(1)).execute(any()) + } + + @Test + fun subscribe_whenStreamErrors_callsOnCompleteEarly() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + + future.complete(erroringStreamResponse) + + inOrder(handler, erroringStreamResponse) { + verify(handler, times(1)).onNext("chunk1") + verify(handler, times(1)).onNext("chunk2") + verify(handler, times(1)).onComplete(Optional.of(ERROR)) + verify(erroringStreamResponse, times(1)).close() + } + verify(executor, times(1)).execute(any()) + } + + @Test + fun onCompleteFuture_whenStreamResponseFutureNotCompleted_onCompleteFutureNotCompleted() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + + val onCompletableFuture = asyncStreamResponse.onCompleteFuture() + + assertThat(onCompletableFuture).isNotCompleted + } + + @Test + fun onCompleteFuture_whenStreamResponseFutureErrors_onCompleteFutureCompletedExceptionally() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + future.completeExceptionally(ERROR) + + val onCompletableFuture = asyncStreamResponse.onCompleteFuture() + + assertThat(onCompletableFuture).isCompletedExceptionally + } + + @Test + fun onCompleteFuture_whenStreamResponseFutureCompletedButStillStreaming_onCompleteFutureNotCompleted() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + future.complete(streamResponse) + + val onCompletableFuture = asyncStreamResponse.onCompleteFuture() + + assertThat(onCompletableFuture).isNotCompleted + } + + @Test + fun onCompleteFuture_whenStreamResponseFutureCompletedAndStreamErrors_onCompleteFutureCompletedExceptionally() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + future.complete(erroringStreamResponse) + + val onCompletableFuture = asyncStreamResponse.onCompleteFuture() + + assertThat(onCompletableFuture).isCompletedExceptionally + } + + @Test + fun onCompleteFuture_whenStreamResponseFutureCompletedAndStreamCompleted_onCompleteFutureCompleted() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe(handler) + future.complete(streamResponse) + + val onCompletableFuture = asyncStreamResponse.onCompleteFuture() + + assertThat(onCompletableFuture).isCompleted + } + + @Test + fun onCompleteFuture_whenHandlerOnCompleteWithoutThrowableThrows_onCompleteFutureCompleted() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe( + object : AsyncStreamResponse.Handler { + override fun onNext(value: String) {} + + override fun onComplete(error: Optional) = throw ERROR + } + ) + future.complete(streamResponse) + + val onCompletableFuture = asyncStreamResponse.onCompleteFuture() + + assertThat(onCompletableFuture).isCompleted + } + + @Test + fun onCompleteFuture_whenHandlerOnCompleteWithThrowableThrows_onCompleteFutureCompletedExceptionally() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.subscribe( + object : AsyncStreamResponse.Handler { + override fun onNext(value: String) {} + + override fun onComplete(error: Optional) = throw ERROR + } + ) + future.complete(erroringStreamResponse) + + val onCompletableFuture = asyncStreamResponse.onCompleteFuture() + + assertThat(onCompletableFuture).isCompletedExceptionally + } + + @Test + fun onCompleteFuture_whenClosed_onCompleteFutureCompleted() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.close() + + val onCompletableFuture = asyncStreamResponse.onCompleteFuture() + + assertThat(onCompletableFuture).isCompleted + } + + @Test + fun close_whenNotClosed_closesStreamResponse() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + + asyncStreamResponse.close() + future.complete(streamResponse) + + verify(streamResponse, times(1)).close() + } + + @Test + fun close_whenAlreadyClosed_doesNothing() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.close() + future.complete(streamResponse) + + asyncStreamResponse.close() + + verify(streamResponse, times(1)).close() + } + + @Test + fun close_whenFutureErrors_doesNothing() { + val future = CompletableFuture>() + val asyncStreamResponse = future.toAsync(executor) + asyncStreamResponse.close() + + assertDoesNotThrow { future.completeExceptionally(ERROR) } + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/HeadersTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/HeadersTest.kt new file mode 100644 index 0000000..81b7261 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/HeadersTest.kt @@ -0,0 +1,242 @@ +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 HeadersTest { + + enum class TestCase( + val headers: Headers, + val expectedMap: Map>, + val expectedSize: Int, + ) { + EMPTY(Headers.builder().build(), expectedMap = mapOf(), expectedSize = 0), + PUT_ONE( + Headers.builder().put("name", "value").build(), + expectedMap = mapOf("name" to listOf("value")), + expectedSize = 1, + ), + PUT_MULTIPLE( + Headers.builder().put("name", listOf("value1", "value2")).build(), + expectedMap = mapOf("name" to listOf("value1", "value2")), + expectedSize = 2, + ), + MULTIPLE_PUT( + Headers.builder().put("name1", "value").put("name2", "value").build(), + expectedMap = mapOf("name1" to listOf("value"), "name2" to listOf("value")), + expectedSize = 2, + ), + MULTIPLE_PUT_SAME_NAME( + Headers.builder().put("name", "value1").put("name", "value2").build(), + expectedMap = mapOf("name" to listOf("value1", "value2")), + expectedSize = 2, + ), + MULTIPLE_PUT_MULTIPLE( + Headers.builder() + .put("name", listOf("value1", "value2")) + .put("name", listOf("value1", "value2")) + .build(), + expectedMap = mapOf("name" to listOf("value1", "value2", "value1", "value2")), + expectedSize = 4, + ), + PUT_CASE_INSENSITIVE( + Headers.builder() + .put("name", "value1") + .put("NAME", "value2") + .put("nAmE", "value3") + .build(), + expectedMap = mapOf("name" to listOf("value1", "value2", "value3")), + expectedSize = 3, + ), + PUT_ALL_MAP( + Headers.builder() + .putAll( + mapOf( + "name1" to listOf("value1", "value2"), + "name2" to listOf("value1", "value2"), + ) + ) + .build(), + expectedMap = + mapOf("name1" to listOf("value1", "value2"), "name2" to listOf("value1", "value2")), + expectedSize = 4, + ), + PUT_ALL_HEADERS( + Headers.builder().putAll(Headers.builder().put("name", "value").build()).build(), + expectedMap = mapOf("name" to listOf("value")), + expectedSize = 1, + ), + PUT_ALL_CASE_INSENSITIVE( + Headers.builder() + .putAll( + mapOf( + "name" to listOf("value1"), + "NAME" to listOf("value2"), + "nAmE" to listOf("value3"), + ) + ) + .build(), + expectedMap = mapOf("name" to listOf("value1", "value2", "value3")), + expectedSize = 3, + ), + REMOVE_ABSENT( + Headers.builder().remove("name").build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REMOVE_PRESENT_ONE( + Headers.builder().put("name", "value").remove("name").build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REMOVE_PRESENT_MULTIPLE( + Headers.builder().put("name", listOf("value1", "value2")).remove("name").build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REMOVE_CASE_INSENSITIVE( + Headers.builder().put("name", listOf("value1", "value2")).remove("NAME").build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REMOVE_ALL( + Headers.builder() + .put("name1", "value") + .put("name3", "value") + .removeAll(setOf("name1", "name2", "name3")) + .build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REMOVE_ALL_CASE_INSENSITIVE( + Headers.builder() + .put("name1", "value") + .put("name3", "value") + .removeAll(setOf("NAME1", "nAmE3")) + .build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + CLEAR( + Headers.builder().put("name1", "value").put("name2", "value").clear().build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REPLACE_ONE_ABSENT( + Headers.builder().replace("name", "value").build(), + expectedMap = mapOf("name" to listOf("value")), + expectedSize = 1, + ), + REPLACE_ONE_PRESENT_ONE( + Headers.builder().put("name", "value1").replace("name", "value2").build(), + expectedMap = mapOf("name" to listOf("value2")), + expectedSize = 1, + ), + REPLACE_ONE_PRESENT_MULTIPLE( + Headers.builder() + .put("name", listOf("value1", "value2")) + .replace("name", "value3") + .build(), + expectedMap = mapOf("name" to listOf("value3")), + expectedSize = 1, + ), + REPLACE_MULTIPLE_ABSENT( + Headers.builder().replace("name", listOf("value1", "value2")).build(), + expectedMap = mapOf("name" to listOf("value1", "value2")), + expectedSize = 2, + ), + REPLACE_MULTIPLE_PRESENT_ONE( + Headers.builder() + .put("name", "value1") + .replace("name", listOf("value2", "value3")) + .build(), + expectedMap = mapOf("name" to listOf("value2", "value3")), + expectedSize = 2, + ), + REPLACE_MULTIPLE_PRESENT_MULTIPLE( + Headers.builder() + .put("name", listOf("value1", "value2")) + .replace("name", listOf("value3", "value4")) + .build(), + expectedMap = mapOf("name" to listOf("value3", "value4")), + expectedSize = 2, + ), + REPLACE_CASE_INSENSITIVE( + Headers.builder() + .put("name", "value1") + .replace("NAME", listOf("value2", "value3")) + .build(), + expectedMap = mapOf("NAME" to listOf("value2", "value3")), + expectedSize = 2, + ), + REPLACE_ALL_MAP( + Headers.builder() + .put("name1", "value1") + .put("name2", "value1") + .put("name3", "value1") + .replaceAll(mapOf("name1" to listOf("value2"), "name3" to listOf("value2"))) + .build(), + expectedMap = + mapOf( + "name1" to listOf("value2"), + "name2" to listOf("value1"), + "name3" to listOf("value2"), + ), + expectedSize = 3, + ), + REPLACE_ALL_HEADERS( + Headers.builder() + .put("name1", "value1") + .put("name2", "value1") + .put("name3", "value1") + .replaceAll(Headers.builder().put("name1", "value2").put("name3", "value2").build()) + .build(), + expectedMap = + mapOf( + "name1" to listOf("value2"), + "name2" to listOf("value1"), + "name3" to listOf("value2"), + ), + expectedSize = 3, + ), + REPLACE_ALL_CASE_INSENSITIVE( + Headers.builder() + .put("name1", "value1") + .put("name2", "value1") + .replaceAll(mapOf("NAME1" to listOf("value2"), "nAmE2" to listOf("value2"))) + .build(), + expectedMap = mapOf("NAME1" to listOf("value2"), "nAmE2" to listOf("value2")), + expectedSize = 2, + ), + } + + @ParameterizedTest + @EnumSource + fun namesAndValues(testCase: TestCase) { + val map = mutableMapOf>() + val headers = testCase.headers + headers.names().forEach { name -> map[name] = headers.values(name) } + + assertThat(map).isEqualTo(testCase.expectedMap) + } + + @ParameterizedTest + @EnumSource + fun caseInsensitiveNames(testCase: TestCase) { + val headers = testCase.headers + + for (name in headers.names()) { + assertThat(headers.values(name)).isEqualTo(headers.values(name.lowercase())) + assertThat(headers.values(name)).isEqualTo(headers.values(name.uppercase())) + } + } + + @ParameterizedTest + @EnumSource + fun size(testCase: TestCase) { + val size = testCase.headers.size + + assertThat(size).isEqualTo(testCase.expectedSize) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/HttpRequestBodiesTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/HttpRequestBodiesTest.kt new file mode 100644 index 0000000..2cf6fa4 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/HttpRequestBodiesTest.kt @@ -0,0 +1,729 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.http + +import com.browserbase.api.core.MultipartField +import com.browserbase.api.core.jsonMapper +import java.io.ByteArrayOutputStream +import java.io.InputStream +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class HttpRequestBodiesTest { + + @Test + fun multipartFormData_serializesFieldWithFilename() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "file" to + MultipartField.builder() + .value("hello") + .filename("hello.txt") + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(output.size().toLong()).isEqualTo(body.contentLength()) + val boundary = body.contentType()!!.substringAfter("multipart/form-data; boundary=") + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="file"; filename="hello.txt" + |Content-Type: text/plain + | + |hello + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesFieldWithoutFilename() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "field" to + MultipartField.builder() + .value("value") + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(output.size().toLong()).isEqualTo(body.contentLength()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="field" + |Content-Type: text/plain + | + |value + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesInputStream() { + // Use `.buffered()` to get a non-ByteArrayInputStream, which hits the non-repeatable code + // path. + val inputStream = "stream content".byteInputStream().buffered() + val body = + multipartFormData( + jsonMapper(), + mapOf( + "data" to + MultipartField.builder() + .value(inputStream) + .contentType("application/octet-stream") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isFalse() + assertThat(body.contentLength()).isEqualTo(-1L) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="data" + |Content-Type: application/octet-stream + | + |stream content + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesByteArray() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "binary" to + MultipartField.builder() + .value("abc".toByteArray()) + .contentType("application/octet-stream") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="binary" + |Content-Type: application/octet-stream + | + |abc + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesBooleanValue() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "flag" to + MultipartField.builder() + .value(true) + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="flag" + |Content-Type: text/plain + | + |true + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesNumberValue() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "count" to + MultipartField.builder().value(42).contentType("text/plain").build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="count" + |Content-Type: text/plain + | + |42 + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesNullValueAsNoParts() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "present" to + MultipartField.builder() + .value("yes") + .contentType("text/plain") + .build(), + "absent" to + MultipartField.builder() + .value(null as String?) + .contentType("text/plain") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="present" + |Content-Type: text/plain + | + |yes + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesArray() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "items" to + MultipartField.builder>() + .value(listOf("alpha", "beta", "gamma")) + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="items" + |Content-Type: text/plain + | + |alpha,beta,gamma + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesObjectAsNestedParts() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "meta" to + MultipartField.builder>() + .value(mapOf("key1" to "val1", "key2" to "val2")) + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="meta[key1]" + |Content-Type: text/plain + | + |val1 + |--$boundary + |Content-Disposition: form-data; name="meta[key2]" + |Content-Type: text/plain + | + |val2 + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesMultipleFields() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "name" to + MultipartField.builder() + .value("Alice") + .contentType("text/plain") + .build(), + "age" to + MultipartField.builder().value(30).contentType("text/plain").build(), + "file" to + MultipartField.builder() + .value("file contents") + .filename("doc.txt") + .contentType("text/plain") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="name" + |Content-Type: text/plain + | + |Alice + |--$boundary + |Content-Disposition: form-data; name="age" + |Content-Type: text/plain + | + |30 + |--$boundary + |Content-Disposition: form-data; name="file"; filename="doc.txt" + |Content-Type: text/plain + | + |file contents + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_quotesSpecialCharactersInNameAndFilename() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "field\nname" to + MultipartField.builder() + .value("value") + .filename("file\r\"name.txt") + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="field%0Aname"; filename="file%0D%22name.txt" + |Content-Type: text/plain + | + |value + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_writeIsRepeatable() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "field" to + MultipartField.builder() + .value("repeatable") + .contentType("text/plain") + .build() + ), + ) + + val output1 = ByteArrayOutputStream() + body.writeTo(output1) + val output2 = ByteArrayOutputStream() + body.writeTo(output2) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output1.size().toLong()) + val boundary = boundary(body) + val expected = + """ + |--$boundary + |Content-Disposition: form-data; name="field" + |Content-Type: text/plain + | + |repeatable + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + assertThat(output1.toString("UTF-8")).isEqualTo(expected) + assertThat(output2.toString("UTF-8")).isEqualTo(expected) + } + + @Test + fun multipartFormData_serializesByteArrayInputStream() { + // ByteArrayInputStream is specifically handled as repeatable with known content length. + val inputStream = "byte array stream".byteInputStream() + val body = + multipartFormData( + jsonMapper(), + mapOf( + "data" to + MultipartField.builder() + .value(inputStream) + .contentType("application/octet-stream") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="data" + |Content-Type: application/octet-stream + | + |byte array stream + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesInputStreamWithFilename() { + // Use `.buffered()` to get a non-ByteArrayInputStream, which hits the non-repeatable code + // path. + val inputStream = "file data".byteInputStream().buffered() + val body = + multipartFormData( + jsonMapper(), + mapOf( + "upload" to + MultipartField.builder() + .value(inputStream) + .filename("upload.bin") + .contentType("application/octet-stream") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isFalse() + assertThat(body.contentLength()).isEqualTo(-1L) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="upload"; filename="upload.bin" + |Content-Type: application/octet-stream + | + |file data + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesNestedArrayInObject() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "data" to + MultipartField.builder>>() + .value(mapOf("tags" to listOf("a", "b"))) + .contentType("text/plain") + .build() + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="data[tags]" + |Content-Type: text/plain + | + |a,b + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_contentLengthIsUnknownWhenInputStreamPresent() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "text" to + MultipartField.builder() + .value("hello") + .contentType("text/plain") + .build(), + "stream" to + MultipartField.builder() + // Use `.buffered()` to get a non-ByteArrayInputStream, which hits the + // non-repeatable code path. + .value("data".byteInputStream().buffered()) + .contentType("application/octet-stream") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isFalse() + assertThat(body.contentLength()).isEqualTo(-1L) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="text" + |Content-Type: text/plain + | + |hello + |--$boundary + |Content-Disposition: form-data; name="stream" + |Content-Type: application/octet-stream + | + |data + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesEmptyArray() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "required" to + MultipartField.builder() + .value("present") + .contentType("text/plain") + .build(), + "items" to + MultipartField.builder>() + .value(emptyList()) + .contentType("text/plain") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="required" + |Content-Type: text/plain + | + |present + |--$boundary + |Content-Disposition: form-data; name="items" + |Content-Type: text/plain + | + | + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + @Test + fun multipartFormData_serializesEmptyObject() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "required" to + MultipartField.builder() + .value("present") + .contentType("text/plain") + .build(), + "meta" to + MultipartField.builder>() + .value(emptyMap()) + .contentType("text/plain") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + assertThat(body.repeatable()).isTrue() + assertThat(body.contentLength()).isEqualTo(output.size().toLong()) + val boundary = boundary(body) + assertThat(output.toString("UTF-8")) + .isEqualTo( + """ + |--$boundary + |Content-Disposition: form-data; name="required" + |Content-Type: text/plain + | + |present + |--$boundary-- + | + """ + .trimMargin() + .replace("\n", "\r\n") + ) + } + + private fun boundary(body: HttpRequestBody): String = + body.contentType()!!.substringAfter("multipart/form-data; boundary=") +} 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/core/http/LoggingHttpClientTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/LoggingHttpClientTest.kt new file mode 100644 index 0000000..a35c99d --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/LoggingHttpClientTest.kt @@ -0,0 +1,1009 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.http + +import com.browserbase.api.core.LogLevel +import com.browserbase.api.core.RequestOptions +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.io.PrintStream +import java.nio.charset.StandardCharsets +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset +import java.util.concurrent.CompletableFuture +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.ResourceLock +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +@ResourceLock("stderr") +internal class LoggingHttpClientTest { + + private lateinit var originalErr: PrintStream + private lateinit var errContent: ByteArrayOutputStream + + @BeforeEach + fun beforeEach() { + originalErr = System.err + errContent = ByteArrayOutputStream() + System.setErr(PrintStream(errContent)) + } + + @AfterEach + fun afterEach() { + System.setErr(originalErr) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun offLevel_noOutput(async: Boolean) { + val client = loggingClient(fakeHttpClient(), LogLevel.OFF) + + val response = client.execute(simpleGetRequest(), async).apply { body().readBytes() } + + assertThat(response.statusCode()).isEqualTo(200) + assertThat(stderrOutput()).isEmpty() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun infoLevel_logsGetRequest(async: Boolean) { + val client = loggingClient(fakeHttpClient(), LogLevel.INFO) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |<-- 200 (0s) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun infoLevel_logsPostRequestWithBodySize(async: Boolean) { + val client = loggingClient(fakeHttpClient(), LogLevel.INFO) + + client.execute(postRequestWithBody("""{"key":"value"}"""), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> POST https://api.example.com/v1/resources (15-byte body) + |<-- 200 (0s) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun infoLevel_logsRequestWithUnknownLengthBody(async: Boolean) { + val client = loggingClient(fakeHttpClient(), LogLevel.INFO) + + client + .execute(postRequestWithBody("""{"key":"value"}""", contentLength = -1L), async) + .body() + .readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> POST https://api.example.com/v1/resources (unknown-length body) + |<-- 200 (0s) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun infoLevel_logsResponseStatusAndDuration(async: Boolean) { + val clock = + clockFrom( + Instant.parse("1998-04-21T00:00:00Z"), + Instant.parse("1998-04-21T00:00:01.234Z"), + ) + val client = loggingClient(fakeHttpClient(statusCode = 201), LogLevel.INFO, clock) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |<-- 201 (1s 234ms) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun infoLevel_logsResponseContentLength(async: Boolean) { + val headers = + Headers.builder().put("Content-Length", "42").put("Content-Type", "text/plain").build() + val client = loggingClient(fakeHttpClient(responseHeaders = headers), LogLevel.INFO) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |<-- 200 (0s, 42-byte body) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun infoLevel_doesNotLogHeaders(async: Boolean) { + val headers = Headers.builder().put("X-Custom", "visible").build() + val client = loggingClient(fakeHttpClient(responseHeaders = headers), LogLevel.INFO) + + client + .execute( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("v1") + .putHeader("X-Request-Custom", "req-value") + .build(), + async, + ) + .body() + .readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1 + |<-- 200 (0s) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsGetWithEndMarker(async: Boolean) { + val client = loggingClient(fakeHttpClient(), LogLevel.DEBUG) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + |<-- END HTTP (0-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsRequestAndResponseHeaders(async: Boolean) { + val responseHeaders = + Headers.builder() + .put("X-Response-Id", "abc-123") + .put("Content-Type", "text/plain") + .build() + val client = + loggingClient(fakeHttpClient(responseHeaders = responseHeaders), LogLevel.DEBUG) + + client + .execute( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("test") + .putHeader("X-Custom", "my-value") + .build(), + async, + ) + .body() + .readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/test + |X-Custom: my-value + |--> END GET + | + |<-- 200 (0s) + |Content-Type: text/plain + |X-Response-Id: abc-123 + |<-- END HTTP (0-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_redactsSensitiveHeaders(async: Boolean) { + val client = + loggingClient( + fakeHttpClient(), + LogLevel.DEBUG, + redactedHeaders = setOf("Authorization", "X-Secret"), + ) + + client + .execute( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("test") + .putHeader("Authorization", "Bearer token-123") + .putHeader("X-Secret", "secret-value") + .putHeader("X-Public", "public-value") + .build(), + async, + ) + .body() + .readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/test + |Authorization: ██ + |X-Public: public-value + |X-Secret: ██ + |--> END GET + | + |<-- 200 (0s) + |<-- END HTTP (0-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_redactsHeadersCaseInsensitively(async: Boolean) { + val client = + loggingClient( + fakeHttpClient(), + LogLevel.DEBUG, + redactedHeaders = setOf("Authorization"), + ) + + client + .execute( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("test") + .putHeader("authorization", "Bearer secret") + .build(), + async, + ) + .body() + .readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/test + |authorization: ██ + |--> END GET + | + |<-- 200 (0s) + |<-- END HTTP (0-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsRequestBody(async: Boolean) { + val client = loggingClient(fakeHttpClient(), LogLevel.DEBUG) + val body = """{"name":"test","value":42}""" + + client.execute(postRequestWithBody(body), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> POST https://api.example.com/v1/resources (26-byte body) + | + |{"name":"test","value":42} + |--> END POST (26-byte body) + | + |<-- 200 (0s) + |<-- END HTTP (0-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsResponseBody(async: Boolean) { + val responseBody = """{"id":1,"status":"ok"}""" + val headers = Headers.builder().put("Content-Type", "application/json").build() + val client = + loggingClient( + fakeHttpClient( + responseHeaders = headers, + responseBody = responseBody.toByteArray(StandardCharsets.UTF_8), + ), + LogLevel.DEBUG, + ) + + val response = client.execute(simpleGetRequest(), async) + val body = response.body().readBytes().toString(StandardCharsets.UTF_8) + + assertThat(body).isEqualTo(responseBody) + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + |Content-Type: application/json + | + |{"id":1,"status":"ok"} + |<-- END HTTP (22-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsBinaryResponseBodyAsOmitted(async: Boolean) { + val binaryBody = ByteArray(256) { it.toByte() } + val client = loggingClient(fakeHttpClient(responseBody = binaryBody), LogLevel.DEBUG) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + | + |(binary body omitted) + |<-- END HTTP (256-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsMultilineResponseBody(async: Boolean) { + val multilineBody = "line1\nline2\nline3" + val headers = Headers.builder().put("Content-Type", "text/plain; charset=utf-8").build() + val client = + loggingClient( + fakeHttpClient( + responseHeaders = headers, + responseBody = multilineBody.toByteArray(StandardCharsets.UTF_8), + ), + LogLevel.DEBUG, + ) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + |Content-Type: text/plain; charset=utf-8 + | + |line1 + |line2 + |line3 + |<-- END HTTP (17-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsResponseBodyWithExplicitCharset(async: Boolean) { + val responseBody = "héllo wörld" + val headers = Headers.builder().put("Content-Type", "text/plain; charset=utf-8").build() + val client = + loggingClient( + fakeHttpClient( + responseHeaders = headers, + responseBody = responseBody.toByteArray(StandardCharsets.UTF_8), + ), + LogLevel.DEBUG, + ) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + |Content-Type: text/plain; charset=utf-8 + | + |héllo wörld + |<-- END HTTP (13-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsResponseBodyWithNoContentType(async: Boolean) { + val responseBody = "plain text body" + val client = + loggingClient( + fakeHttpClient(responseBody = responseBody.toByteArray(StandardCharsets.UTF_8)), + LogLevel.DEBUG, + ) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + | + |plain text body + |<-- END HTTP (15-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsEmptyResponseBody(async: Boolean) { + val client = loggingClient(fakeHttpClient(), LogLevel.DEBUG) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + |<-- END HTTP (0-byte body) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsEndHttpMarkerOnEarlyClose(async: Boolean) { + val responseBody = """{"id":1,"status":"ok"}""" + val headers = Headers.builder().put("Content-Type", "application/json").build() + val client = + loggingClient( + fakeHttpClient( + responseHeaders = headers, + responseBody = responseBody.toByteArray(StandardCharsets.UTF_8), + ), + LogLevel.DEBUG, + ) + + val body = client.execute(simpleGetRequest(), async).body() + body.read(ByteArray(5)) + body.close() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + |Content-Type: application/json + | + |{"id" + |<-- END HTTP (5-byte body, closed early) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsEndHttpMarkerOnCloseWithoutReading(async: Boolean) { + val responseBody = """{"id":1,"status":"ok"}""" + val headers = Headers.builder().put("Content-Type", "application/json").build() + val client = + loggingClient( + fakeHttpClient( + responseHeaders = headers, + responseBody = responseBody.toByteArray(StandardCharsets.UTF_8), + ), + LogLevel.DEBUG, + ) + + client.execute(simpleGetRequest(), async).body().close() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + |Content-Type: application/json + |<-- END HTTP (0-byte body, closed early) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsEndHttpMarkerWhenResponseClosedAfterPartialRead(async: Boolean) { + val responseBody = """{"id":1,"status":"ok"}""" + val headers = Headers.builder().put("Content-Type", "application/json").build() + val client = + loggingClient( + fakeHttpClient( + responseHeaders = headers, + responseBody = responseBody.toByteArray(StandardCharsets.UTF_8), + ), + LogLevel.DEBUG, + ) + + val response = client.execute(simpleGetRequest(), async) + response.body().read(ByteArray(5)) + response.close() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + |Content-Type: application/json + | + |{"id" + |<-- END HTTP (5-byte body, closed early) + | + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_doesNotLogEndHttpMarkerWhenResponseClosedWithoutBodyAccess(async: Boolean) { + val responseBody = """{"id":1,"status":"ok"}""" + val headers = Headers.builder().put("Content-Type", "application/json").build() + val client = + loggingClient( + fakeHttpClient( + responseHeaders = headers, + responseBody = responseBody.toByteArray(StandardCharsets.UTF_8), + ), + LogLevel.DEBUG, + ) + + client.execute(simpleGetRequest(), async).close() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- 200 (0s) + |Content-Type: application/json + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun errorLevel_logsRequestFailure(async: Boolean) { + val clock = + clockFrom( + Instant.parse("1998-04-21T00:00:00Z"), + Instant.parse("1998-04-21T00:00:01.234Z"), + ) + val client = + loggingClient( + failingHttpClient(IOException("Connection refused")), + LogLevel.ERROR, + clock, + ) + + assertThatThrownBy { client.execute(simpleGetRequest(), async) } + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |<-- !! IOException: Connection refused (1s 234ms) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun infoLevel_doesNotLogRequestFailure(async: Boolean) { + val client = + loggingClient(failingHttpClient(IOException("Connection refused")), LogLevel.INFO) + + assertThatThrownBy { client.execute(simpleGetRequest(), async) } + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun debugLevel_logsRequestFailureAfterHeaders(async: Boolean) { + val client = + loggingClient(failingHttpClient(IOException("Connection refused")), LogLevel.DEBUG) + + assertThatThrownBy { client.execute(simpleGetRequest(), async) } + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |--> END GET + | + |<-- !! IOException: Connection refused (0s) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun errorLevel_logsRequestFailureWithoutMessage(async: Boolean) { + val client = loggingClient(failingHttpClient(IOException()), LogLevel.ERROR) + + assertThatThrownBy { client.execute(simpleGetRequest(), async) } + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |<-- !! IOException (0s) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun offLevel_doesNotLogRequestFailure(async: Boolean) { + val client = + loggingClient(failingHttpClient(IOException("Connection refused")), LogLevel.OFF) + + assertThatThrownBy { client.execute(simpleGetRequest(), async) } + + assertThat(stderrOutput()).isEmpty() + } + + @Test + fun errorLevel_logsExecuteAsyncSynchronousThrow() { + val error = IOException("Connection refused") + val client = + loggingClient( + object : HttpClient { + override fun execute( + request: HttpRequest, + requestOptions: RequestOptions, + ): HttpResponse = throw UnsupportedOperationException() + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture = throw error + + override fun close() {} + }, + LogLevel.ERROR, + ) + + assertThatThrownBy { client.execute(simpleGetRequest(), async = true) }.isSameAs(error) + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |<-- !! IOException: Connection refused (0s) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun durationFormat_seconds(async: Boolean) { + val clock = + clockFrom( + Instant.parse("1998-04-21T00:00:00Z"), + Instant.parse("1998-04-21T00:00:02.500Z"), + ) + val client = loggingClient(fakeHttpClient(), LogLevel.INFO, clock) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |<-- 200 (2s 500ms) + |""" + .trimMargin() + ) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun durationFormat_minutesAndSeconds(async: Boolean) { + val clock = + clockFrom( + Instant.parse("1998-04-21T00:00:00Z"), + Instant.parse("1998-04-21T00:01:40.467Z"), + ) + val client = loggingClient(fakeHttpClient(), LogLevel.INFO, clock) + + client.execute(simpleGetRequest(), async).body().readBytes() + + assertThat(stderrOutput()) + .isEqualTo( + """ + |--> GET https://api.example.com/v1/resources + |<-- 200 (1m 40s 467ms) + |""" + .trimMargin() + ) + } + + @Test + fun builder_toBuilder_roundtrips() { + val delegate = fakeHttpClient() + val clock = Clock.fixed(Instant.parse("1998-04-21T00:00:00Z"), ZoneOffset.UTC) + val client = + LoggingHttpClient.builder() + .httpClient(delegate) + .level(LogLevel.DEBUG) + .redactedHeaders(setOf("X-Secret")) + .clock(clock) + .build() + + val rebuilt = client.toBuilder().build() + + assertThat(rebuilt.httpClient).isSameAs(delegate) + assertThat(rebuilt.level).isEqualTo(LogLevel.DEBUG) + assertThat(rebuilt.redactedHeaders).containsExactly("X-Secret") + assertThat(rebuilt.clock).isEqualTo(clock) + } + + @Test + fun close_delegatesToUnderlyingClient() { + var closed = false + val delegate = + object : HttpClient { + override fun execute( + request: HttpRequest, + requestOptions: RequestOptions, + ): HttpResponse = throw UnsupportedOperationException() + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture = throw UnsupportedOperationException() + + override fun close() { + closed = true + } + } + val client = loggingClient(delegate, LogLevel.OFF) + + client.close() + + assertThat(closed).isTrue() + } + + private fun stderrOutput(): String = errContent.toString("UTF-8") + + private fun loggingClient( + httpClient: HttpClient, + level: LogLevel, + clock: Clock = clockFrom(Instant.parse("1998-04-21T00:00:00Z")), + redactedHeaders: Set = + setOf( + "authorization", + "api-key", + "x-api-key", + "cookie", + "set-cookie", + "x-bb-api-key", + "x-bb-project-id", + "x-model-api-key", + ), + ): LoggingHttpClient = + LoggingHttpClient.builder() + .httpClient(httpClient) + .level(level) + .clock(clock) + .redactedHeaders(redactedHeaders) + .build() + + private fun simpleGetRequest(): HttpRequest = + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("v1") + .addPathSegment("resources") + .build() + + private fun postRequestWithBody( + body: String, + contentType: String = "application/json", + contentLength: Long? = null, + ): HttpRequest = + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl("https://api.example.com") + .addPathSegment("v1") + .addPathSegment("resources") + .body( + object : HttpRequestBody { + private val bytes = body.toByteArray(StandardCharsets.UTF_8) + + override fun writeTo(outputStream: OutputStream) { + outputStream.write(bytes) + } + + override fun contentType(): String = contentType + + override fun contentLength(): Long = contentLength ?: bytes.size.toLong() + + override fun repeatable(): Boolean = true + + override fun close() {} + } + ) + .build() + + private fun fakeHttpClient( + statusCode: Int = 200, + responseHeaders: Headers = Headers.builder().build(), + responseBody: ByteArray = ByteArray(0), + ): HttpClient = + object : HttpClient { + override fun execute( + request: HttpRequest, + requestOptions: RequestOptions, + ): HttpResponse { + // Consume the request body if present to trigger logging. + request.body?.let { + val out = ByteArrayOutputStream() + it.writeTo(out) + } + return fakeResponse(statusCode, responseHeaders, responseBody) + } + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture = + CompletableFuture.completedFuture(execute(request, requestOptions)) + + override fun close() {} + } + + private fun failingHttpClient(error: Throwable): HttpClient = + object : HttpClient { + override fun execute( + request: HttpRequest, + requestOptions: RequestOptions, + ): HttpResponse { + request.body?.let { + val out = ByteArrayOutputStream() + it.writeTo(out) + } + throw error + } + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture { + val future = CompletableFuture() + future.completeExceptionally(error) + return future + } + + override fun close() {} + } + + private fun fakeResponse(statusCode: Int, headers: Headers, body: ByteArray): HttpResponse = + object : HttpResponse { + override fun statusCode(): Int = statusCode + + override fun headers(): Headers = headers + + override fun body(): InputStream = ByteArrayInputStream(body) + + override fun close() {} + } + + private fun clockFrom(vararg instants: Instant): Clock = + object : Clock() { + private var index = 0 + + override fun getZone() = ZoneOffset.UTC + + override fun withZone(zone: java.time.ZoneId?) = this + + override fun instant(): Instant { + val instant = instants[index % instants.size] + index++ + return instant + } + } + + private fun HttpClient.execute(request: HttpRequest, async: Boolean): HttpResponse = + if (async) executeAsync(request).get() else execute(request) +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/QueryParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/QueryParamsTest.kt new file mode 100644 index 0000000..8d98e38 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/QueryParamsTest.kt @@ -0,0 +1,180 @@ +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 QueryParamsTest { + + enum class TestCase( + val queryParams: QueryParams, + val expectedMap: Map>, + val expectedSize: Int, + ) { + EMPTY(QueryParams.builder().build(), expectedMap = mapOf(), expectedSize = 0), + PUT_ONE( + QueryParams.builder().put("key", "value").build(), + expectedMap = mapOf("key" to listOf("value")), + expectedSize = 1, + ), + PUT_MULTIPLE( + QueryParams.builder().put("key", listOf("value1", "value2")).build(), + expectedMap = mapOf("key" to listOf("value1", "value2")), + expectedSize = 2, + ), + MULTIPLE_PUT( + QueryParams.builder().put("key1", "value").put("key2", "value").build(), + expectedMap = mapOf("key1" to listOf("value"), "key2" to listOf("value")), + expectedSize = 2, + ), + MULTIPLE_PUT_SAME_NAME( + QueryParams.builder().put("key", "value1").put("key", "value2").build(), + expectedMap = mapOf("key" to listOf("value1", "value2")), + expectedSize = 2, + ), + MULTIPLE_PUT_MULTIPLE( + QueryParams.builder() + .put("key", listOf("value1", "value2")) + .put("key", listOf("value1", "value2")) + .build(), + expectedMap = mapOf("key" to listOf("value1", "value2", "value1", "value2")), + expectedSize = 4, + ), + PUT_ALL_MAP( + QueryParams.builder() + .putAll( + mapOf( + "key1" to listOf("value1", "value2"), + "key2" to listOf("value1", "value2"), + ) + ) + .build(), + expectedMap = + mapOf("key1" to listOf("value1", "value2"), "key2" to listOf("value1", "value2")), + expectedSize = 4, + ), + PUT_ALL_HEADERS( + QueryParams.builder().putAll(QueryParams.builder().put("key", "value").build()).build(), + expectedMap = mapOf("key" to listOf("value")), + expectedSize = 1, + ), + REMOVE_ABSENT( + QueryParams.builder().remove("key").build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REMOVE_PRESENT_ONE( + QueryParams.builder().put("key", "value").remove("key").build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REMOVE_PRESENT_MULTIPLE( + QueryParams.builder().put("key", listOf("value1", "value2")).remove("key").build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REMOVE_ALL( + QueryParams.builder() + .put("key1", "value") + .put("key3", "value") + .removeAll(setOf("key1", "key2", "key3")) + .build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + CLEAR( + QueryParams.builder().put("key1", "value").put("key2", "value").clear().build(), + expectedMap = mapOf(), + expectedSize = 0, + ), + REPLACE_ONE_ABSENT( + QueryParams.builder().replace("key", "value").build(), + expectedMap = mapOf("key" to listOf("value")), + expectedSize = 1, + ), + REPLACE_ONE_PRESENT_ONE( + QueryParams.builder().put("key", "value1").replace("key", "value2").build(), + expectedMap = mapOf("key" to listOf("value2")), + expectedSize = 1, + ), + REPLACE_ONE_PRESENT_MULTIPLE( + QueryParams.builder() + .put("key", listOf("value1", "value2")) + .replace("key", "value3") + .build(), + expectedMap = mapOf("key" to listOf("value3")), + expectedSize = 1, + ), + REPLACE_MULTIPLE_ABSENT( + QueryParams.builder().replace("key", listOf("value1", "value2")).build(), + expectedMap = mapOf("key" to listOf("value1", "value2")), + expectedSize = 2, + ), + REPLACE_MULTIPLE_PRESENT_ONE( + QueryParams.builder() + .put("key", "value1") + .replace("key", listOf("value2", "value3")) + .build(), + expectedMap = mapOf("key" to listOf("value2", "value3")), + expectedSize = 2, + ), + REPLACE_MULTIPLE_PRESENT_MULTIPLE( + QueryParams.builder() + .put("key", listOf("value1", "value2")) + .replace("key", listOf("value3", "value4")) + .build(), + expectedMap = mapOf("key" to listOf("value3", "value4")), + expectedSize = 2, + ), + REPLACE_ALL_MAP( + QueryParams.builder() + .put("key1", "value1") + .put("key2", "value1") + .put("key3", "value1") + .replaceAll(mapOf("key1" to listOf("value2"), "key3" to listOf("value2"))) + .build(), + expectedMap = + mapOf( + "key1" to listOf("value2"), + "key2" to listOf("value1"), + "key3" to listOf("value2"), + ), + expectedSize = 3, + ), + REPLACE_ALL_HEADERS( + QueryParams.builder() + .put("key1", "value1") + .put("key2", "value1") + .put("key3", "value1") + .replaceAll( + QueryParams.builder().put("key1", "value2").put("key3", "value2").build() + ) + .build(), + expectedMap = + mapOf( + "key1" to listOf("value2"), + "key2" to listOf("value1"), + "key3" to listOf("value2"), + ), + expectedSize = 3, + ), + } + + @ParameterizedTest + @EnumSource + fun keysAndValues(testCase: TestCase) { + val map = mutableMapOf>() + val queryParams = testCase.queryParams + queryParams.keys().forEach { key -> map[key] = queryParams.values(key) } + + assertThat(map).isEqualTo(testCase.expectedMap) + } + + @ParameterizedTest + @EnumSource + fun size(testCase: TestCase) { + val size = testCase.queryParams.size + + assertThat(size).isEqualTo(testCase.expectedSize) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/RetryingHttpClientTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/RetryingHttpClientTest.kt new file mode 100644 index 0000000..49e0138 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/RetryingHttpClientTest.kt @@ -0,0 +1,525 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.core.http + +import com.browserbase.api.client.okhttp.OkHttpClient +import com.browserbase.api.core.RequestOptions +import com.browserbase.api.core.Sleeper +import com.browserbase.api.errors.StagehandRetryableException +import com.github.tomakehurst.wiremock.client.WireMock.equalTo +import com.github.tomakehurst.wiremock.client.WireMock.matching +import com.github.tomakehurst.wiremock.client.WireMock.ok +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.resetAllScenarios +import com.github.tomakehurst.wiremock.client.WireMock.serviceUnavailable +import com.github.tomakehurst.wiremock.client.WireMock.stubFor +import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo +import com.github.tomakehurst.wiremock.client.WireMock.verify +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo +import com.github.tomakehurst.wiremock.junit5.WireMockTest +import com.github.tomakehurst.wiremock.stubbing.Scenario +import java.io.InputStream +import java.time.Clock +import java.time.Duration +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.concurrent.CompletableFuture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.parallel.ResourceLock +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +@WireMockTest +@ResourceLock("https://github.com/wiremock/wiremock/issues/169") +internal class RetryingHttpClientTest { + + private var openResponseCount = 0 + private lateinit var baseUrl: String + private lateinit var httpClient: HttpClient + + private class RecordingSleeper : Sleeper { + val durations = mutableListOf() + + override fun sleep(duration: Duration) { + durations.add(duration) + } + + override fun sleepAsync(duration: Duration): CompletableFuture { + durations.add(duration) + return CompletableFuture.completedFuture(null) + } + + override fun close() {} + } + + @BeforeEach + fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) { + baseUrl = wmRuntimeInfo.httpBaseUrl + val okHttpClient = OkHttpClient.builder().build() + httpClient = + object : HttpClient { + + override fun execute( + request: HttpRequest, + requestOptions: RequestOptions, + ): HttpResponse = trackClose(okHttpClient.execute(request, requestOptions)) + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture = + okHttpClient.executeAsync(request, requestOptions).thenApply { trackClose(it) } + + override fun close() = okHttpClient.close() + + private fun trackClose(response: HttpResponse): HttpResponse { + openResponseCount++ + return object : HttpResponse { + + private var isClosed = false + + override fun statusCode(): Int = response.statusCode() + + override fun headers(): Headers = response.headers() + + override fun body(): InputStream = response.body() + + override fun close() { + response.close() + if (isClosed) { + return + } + openResponseCount-- + isClosed = true + } + } + } + } + resetAllScenarios() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute(async: Boolean) { + stubFor(post(urlPathEqualTo("/something")).willReturn(ok())) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + verify(1, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).isEmpty() + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withIdempotencyHeader(async: Boolean) { + stubFor( + post(urlPathEqualTo("/something")) + .withHeader("X-Some-Header", matching("stainless-java-retry-.+")) + .willReturn(ok()) + ) + val sleeper = RecordingSleeper() + val retryingClient = + retryingHttpClientBuilder(sleeper) + .maxRetries(2) + .idempotencyHeader("X-Some-Header") + .build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + verify(1, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).isEmpty() + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryAfterHeader(async: Boolean) { + val retryAfterDate = "Wed, 21 Oct 2015 07:28:00 GMT" + stubFor( + post(urlPathEqualTo("/something")) + // First we fail with a retry after header given as a date + .inScenario("foo") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(serviceUnavailable().withHeader("Retry-After", retryAfterDate)) + .willSetStateTo("RETRY_AFTER_DATE") + ) + stubFor( + post(urlPathEqualTo("/something")) + // Then we fail with a retry after header given as a delay + .inScenario("foo") + .whenScenarioStateIs("RETRY_AFTER_DATE") + .willReturn(serviceUnavailable().withHeader("Retry-After", "1.234")) + .willSetStateTo("RETRY_AFTER_DELAY") + ) + stubFor( + post(urlPathEqualTo("/something")) + // Then we return a success + .inScenario("foo") + .whenScenarioStateIs("RETRY_AFTER_DELAY") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + // Fix the clock to 5 seconds before the Retry-After date so the date-based backoff is + // deterministic. + val retryAfterDateTime = + OffsetDateTime.parse(retryAfterDate, DateTimeFormatter.RFC_1123_DATE_TIME) + val clock = Clock.fixed(retryAfterDateTime.minusSeconds(5).toInstant(), ZoneOffset.UTC) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper, clock).maxRetries(2).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + verify( + 1, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("0")), + ) + verify( + 1, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("1")), + ) + verify( + 1, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("2")), + ) + assertThat(sleeper.durations) + .containsExactly(Duration.ofSeconds(5), Duration.ofMillis(1234)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withOverwrittenRetryCountHeader(async: Boolean) { + val retryAfterDate = "Wed, 21 Oct 2015 07:28:00 GMT" + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") // first we fail with a retry after header given as a date + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(serviceUnavailable().withHeader("Retry-After", retryAfterDate)) + .willSetStateTo("RETRY_AFTER_DATE") + ) + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") // then we return a success + .whenScenarioStateIs("RETRY_AFTER_DATE") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + val retryAfterDateTime = + OffsetDateTime.parse(retryAfterDate, DateTimeFormatter.RFC_1123_DATE_TIME) + val clock = Clock.fixed(retryAfterDateTime.minusSeconds(5).toInstant(), ZoneOffset.UTC) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper, clock).maxRetries(2).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .putHeader("x-stainless-retry-count", "42") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + verify( + 2, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("42")), + ) + assertThat(sleeper.durations).containsExactly(Duration.ofSeconds(5)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryAfterMsHeader(async: Boolean) { + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(serviceUnavailable().withHeader("Retry-After-Ms", "10")) + .willSetStateTo("RETRY_AFTER_DELAY") + ) + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") // then we return a success + .whenScenarioStateIs("RETRY_AFTER_DELAY") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + verify(2, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).containsExactly(Duration.ofMillis(10)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryableException(async: Boolean) { + stubFor(post(urlPathEqualTo("/something")).willReturn(ok())) + + var callCount = 0 + val failingHttpClient = + object : HttpClient { + override fun execute( + request: HttpRequest, + requestOptions: RequestOptions, + ): HttpResponse { + callCount++ + if (callCount == 1) { + throw StagehandRetryableException("Simulated retryable failure") + } + return httpClient.execute(request, requestOptions) + } + + override fun executeAsync( + request: HttpRequest, + requestOptions: RequestOptions, + ): CompletableFuture { + callCount++ + if (callCount == 1) { + val future = CompletableFuture() + future.completeExceptionally( + StagehandRetryableException("Simulated retryable failure") + ) + return future + } + return httpClient.executeAsync(request, requestOptions) + } + + override fun close() = httpClient.close() + } + + val sleeper = RecordingSleeper() + val retryingClient = + RetryingHttpClient.builder() + .httpClient(failingHttpClient) + .maxRetries(2) + .sleeper(sleeper) + .build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + verify( + 1, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("1")), + ) + verify( + 0, + postRequestedFor(urlPathEqualTo("/something")) + .withHeader("x-stainless-retry-count", equalTo("0")), + ) + // Exponential backoff with jitter: 0.5s * jitter where jitter is in [0.75, 1.0]. + assertThat(sleeper.durations).hasSize(1) + assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withExponentialBackoff(async: Boolean) { + stubFor(post(urlPathEqualTo("/something")).willReturn(serviceUnavailable())) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(3).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + // All retries exhausted; the last 503 response is returned. + assertThat(response.statusCode()).isEqualTo(503) + verify(4, postRequestedFor(urlPathEqualTo("/something"))) + // Exponential backoff with jitter: backoff = min(0.5 * 2^(retries-1), 8) * jitter where + // jitter is in [0.75, 1.0]. + assertThat(sleeper.durations).hasSize(3) + // retries=1: 0.5s * [0.75, 1.0] + assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500)) + // retries=2: 1s * [0.75, 1.0] + assertThat(sleeper.durations[1]).isBetween(Duration.ofMillis(750), Duration.ofMillis(1000)) + // retries=3: 2s * [0.75, 1.0] + assertThat(sleeper.durations[2]).isBetween(Duration.ofMillis(1500), Duration.ofMillis(2000)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withExponentialBackoffCap(async: Boolean) { + stubFor(post(urlPathEqualTo("/something")).willReturn(serviceUnavailable())) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(6).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(503) + verify(7, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).hasSize(6) + // retries=5: backoff hits the 8s cap * [0.75, 1.0] + assertThat(sleeper.durations[4]).isBetween(Duration.ofMillis(6000), Duration.ofMillis(8000)) + // retries=6: still capped at 8s * [0.75, 1.0] + assertThat(sleeper.durations[5]).isBetween(Duration.ofMillis(6000), Duration.ofMillis(8000)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryAfterMsPriorityOverRetryAfter(async: Boolean) { + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn( + serviceUnavailable() + .withHeader("Retry-After-Ms", "50") + .withHeader("Retry-After", "2") + ) + .willSetStateTo("RETRY") + ) + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs("RETRY") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + // Retry-After-Ms (50ms) takes priority over Retry-After (2s). + assertThat(sleeper.durations).containsExactly(Duration.ofMillis(50)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryAfterUnparseable(async: Boolean) { + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(serviceUnavailable().withHeader("Retry-After", "not-a-date-or-number")) + .willSetStateTo("RETRY") + ) + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs("RETRY") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + // Unparseable Retry-After falls through to exponential backoff. + assertThat(sleeper.durations).hasSize(1) + assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500)) + assertNoResponseLeaks() + } + + private fun retryingHttpClientBuilder( + sleeper: RecordingSleeper, + clock: Clock = Clock.systemUTC(), + ) = RetryingHttpClient.builder().httpClient(httpClient).sleeper(sleeper).clock(clock) + + private fun HttpClient.execute(request: HttpRequest, async: Boolean): HttpResponse = + if (async) executeAsync(request).get() else execute(request) + + // When retrying, all failed responses should be closed. Only the final returned response should + // be open. + private fun assertNoResponseLeaks() = assertThat(openResponseCount).isEqualTo(1) +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ActionTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ActionTest.kt new file mode 100644 index 0000000..c76617d --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ActionTest.kt @@ -0,0 +1,48 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import kotlin.jvm.optionals.getOrNull +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class ActionTest { + + @Test + fun create() { + val action = + Action.builder() + .description("Click the submit button") + .selector("[data-testid='submit-button']") + .addArgument("Hello World") + .backendNodeId(0.0) + .method("click") + .build() + + assertThat(action.description()).isEqualTo("Click the submit button") + assertThat(action.selector()).isEqualTo("[data-testid='submit-button']") + assertThat(action.arguments().getOrNull()).containsExactly("Hello World") + assertThat(action.backendNodeId()).contains(0.0) + assertThat(action.method()).contains("click") + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val action = + Action.builder() + .description("Click the submit button") + .selector("[data-testid='submit-button']") + .addArgument("Hello World") + .backendNodeId(0.0) + .method("click") + .build() + + val roundtrippedAction = + jsonMapper.readValue(jsonMapper.writeValueAsString(action), jacksonTypeRef()) + + assertThat(roundtrippedAction).isEqualTo(action) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ModelConfigTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ModelConfigTest.kt new file mode 100644 index 0000000..6e3b3d0 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ModelConfigTest.kt @@ -0,0 +1,64 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class ModelConfigTest { + + @Test + fun create() { + val modelConfig = + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + + assertThat(modelConfig.modelName()).isEqualTo("openai/gpt-5.4-mini") + assertThat(modelConfig.apiKey()).contains("sk-some-openai-api-key") + assertThat(modelConfig.baseUrl()).contains("https://api.openai.com/v1") + assertThat(modelConfig.headers()) + .contains( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + assertThat(modelConfig.provider()).contains(ModelConfig.Provider.OPENAI) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val modelConfig = + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + + val roundtrippedModelConfig = + jsonMapper.readValue( + jsonMapper.writeValueAsString(modelConfig), + jacksonTypeRef(), + ) + + assertThat(roundtrippedModelConfig).isEqualTo(modelConfig) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActParamsTest.kt new file mode 100644 index 0000000..31ef59b --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActParamsTest.kt @@ -0,0 +1,222 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.http.Headers +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionActParamsTest { + + @Test + fun create() { + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionActParams.XStreamResponse.TRUE) + .input("Click the login button") + .frameId("frameId") + .options( + SessionActParams.Options.builder() + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .timeout(30000.0) + .variables( + SessionActParams.Options.Variables.builder() + .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) + .build() + ) + .build() + ) + .build() + } + + @Test + fun pathParams() { + val params = + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .input("Click the login button") + .build() + + assertThat(params._pathParam(0)).isEqualTo("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + // out-of-bound path param + assertThat(params._pathParam(1)).isEqualTo("") + } + + @Test + fun headers() { + val params = + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionActParams.XStreamResponse.TRUE) + .input("Click the login button") + .frameId("frameId") + .options( + SessionActParams.Options.builder() + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .timeout(30000.0) + .variables( + SessionActParams.Options.Variables.builder() + .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) + .build() + ) + .build() + ) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) + } + + @Test + fun headersWithoutOptionalFields() { + val params = + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .input("Click the login button") + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().build()) + } + + @Test + fun body() { + val params = + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionActParams.XStreamResponse.TRUE) + .input("Click the login button") + .frameId("frameId") + .options( + SessionActParams.Options.builder() + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .timeout(30000.0) + .variables( + SessionActParams.Options.Variables.builder() + .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) + .build() + ) + .build() + ) + .build() + + val body = params._body() + + assertThat(body.input()) + .isEqualTo(SessionActParams.Input.ofString("Click the login button")) + assertThat(body.frameId()).contains("frameId") + assertThat(body.options()) + .contains( + SessionActParams.Options.builder() + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .timeout(30000.0) + .variables( + SessionActParams.Options.Variables.builder() + .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) + .build() + ) + .build() + ) + } + + @Test + fun bodyWithoutOptionalFields() { + val params = + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .input("Click the login button") + .build() + + val body = params._body() + + assertThat(body.input()) + .isEqualTo(SessionActParams.Input.ofString("Click the login button")) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActResponseTest.kt new file mode 100644 index 0000000..52cc63c --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActResponseTest.kt @@ -0,0 +1,102 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionActResponseTest { + + @Test + fun create() { + val sessionActResponse = + SessionActResponse.builder() + .data( + SessionActResponse.Data.builder() + .result( + SessionActResponse.Data.Result.builder() + .actionDescription("Clicked button with text 'Login'") + .addAction( + SessionActResponse.Data.Result.Action.builder() + .description("Click the submit button") + .selector("[data-testid='submit-button']") + .addArgument("Hello World") + .backendNodeId(0.0) + .method("click") + .build() + ) + .message("Successfully clicked the login button") + .success(true) + .build() + ) + .actionId("actionId") + .build() + ) + .success(true) + .build() + + assertThat(sessionActResponse.data()) + .isEqualTo( + SessionActResponse.Data.builder() + .result( + SessionActResponse.Data.Result.builder() + .actionDescription("Clicked button with text 'Login'") + .addAction( + SessionActResponse.Data.Result.Action.builder() + .description("Click the submit button") + .selector("[data-testid='submit-button']") + .addArgument("Hello World") + .backendNodeId(0.0) + .method("click") + .build() + ) + .message("Successfully clicked the login button") + .success(true) + .build() + ) + .actionId("actionId") + .build() + ) + assertThat(sessionActResponse.success()).isEqualTo(true) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val sessionActResponse = + SessionActResponse.builder() + .data( + SessionActResponse.Data.builder() + .result( + SessionActResponse.Data.Result.builder() + .actionDescription("Clicked button with text 'Login'") + .addAction( + SessionActResponse.Data.Result.Action.builder() + .description("Click the submit button") + .selector("[data-testid='submit-button']") + .addArgument("Hello World") + .backendNodeId(0.0) + .method("click") + .build() + ) + .message("Successfully clicked the login button") + .success(true) + .build() + ) + .actionId("actionId") + .build() + ) + .success(true) + .build() + + val roundtrippedSessionActResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(sessionActResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSessionActResponse).isEqualTo(sessionActResponse) + } +} 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 new file mode 100644 index 0000000..43a69e2 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndParamsTest.kt @@ -0,0 +1,49 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.http.Headers +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionEndParamsTest { + + @Test + fun create() { + SessionEndParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) + .build() + } + + @Test + fun pathParams() { + val params = SessionEndParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + assertThat(params._pathParam(0)).isEqualTo("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + // out-of-bound path param + assertThat(params._pathParam(1)).isEqualTo("") + } + + @Test + fun headers() { + val params = + SessionEndParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) + } + + @Test + fun headersWithoutOptionalFields() { + val params = SessionEndParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().build()) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndResponseTest.kt new file mode 100644 index 0000000..041295f --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndResponseTest.kt @@ -0,0 +1,32 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionEndResponseTest { + + @Test + fun create() { + val sessionEndResponse = SessionEndResponse.builder().success(true).build() + + assertThat(sessionEndResponse.success()).isEqualTo(true) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val sessionEndResponse = SessionEndResponse.builder().success(true).build() + + val roundtrippedSessionEndResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(sessionEndResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSessionEndResponse).isEqualTo(sessionEndResponse) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteParamsTest.kt new file mode 100644 index 0000000..da74169 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteParamsTest.kt @@ -0,0 +1,322 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.http.Headers +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionExecuteParamsTest { + + @Test + fun create() { + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) + .agentConfig( + SessionExecuteParams.AgentConfig.builder() + .cua(true) + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) + .systemPrompt("systemPrompt") + .build() + ) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .highlightCursor(true) + .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) + .variables( + SessionExecuteParams.ExecuteOptions.Variables.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .build() + ) + .frameId("frameId") + .shouldCache(true) + .build() + } + + @Test + fun pathParams() { + val params = + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .agentConfig(SessionExecuteParams.AgentConfig.builder().build()) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .build() + ) + .build() + + assertThat(params._pathParam(0)).isEqualTo("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + // out-of-bound path param + assertThat(params._pathParam(1)).isEqualTo("") + } + + @Test + fun headers() { + val params = + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) + .agentConfig( + SessionExecuteParams.AgentConfig.builder() + .cua(true) + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) + .systemPrompt("systemPrompt") + .build() + ) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .highlightCursor(true) + .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) + .variables( + SessionExecuteParams.ExecuteOptions.Variables.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .build() + ) + .frameId("frameId") + .shouldCache(true) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) + } + + @Test + fun headersWithoutOptionalFields() { + val params = + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .agentConfig(SessionExecuteParams.AgentConfig.builder().build()) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .build() + ) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().build()) + } + + @Test + fun body() { + val params = + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) + .agentConfig( + SessionExecuteParams.AgentConfig.builder() + .cua(true) + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) + .systemPrompt("systemPrompt") + .build() + ) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .highlightCursor(true) + .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) + .variables( + SessionExecuteParams.ExecuteOptions.Variables.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .build() + ) + .frameId("frameId") + .shouldCache(true) + .build() + + val body = params._body() + + assertThat(body.agentConfig()) + .isEqualTo( + SessionExecuteParams.AgentConfig.builder() + .cua(true) + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) + .systemPrompt("systemPrompt") + .build() + ) + assertThat(body.executeOptions()) + .isEqualTo( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .highlightCursor(true) + .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) + .variables( + SessionExecuteParams.ExecuteOptions.Variables.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .build() + ) + assertThat(body.frameId()).contains("frameId") + assertThat(body.shouldCache()).contains(true) + } + + @Test + fun bodyWithoutOptionalFields() { + val params = + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .agentConfig(SessionExecuteParams.AgentConfig.builder().build()) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .build() + ) + .build() + + val body = params._body() + + assertThat(body.agentConfig()).isEqualTo(SessionExecuteParams.AgentConfig.builder().build()) + assertThat(body.executeOptions()) + .isEqualTo( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .build() + ) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponseTest.kt new file mode 100644 index 0000000..e88a30e --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponseTest.kt @@ -0,0 +1,169 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionExecuteResponseTest { + + @Test + fun create() { + val sessionExecuteResponse = + SessionExecuteResponse.builder() + .data( + SessionExecuteResponse.Data.builder() + .result( + SessionExecuteResponse.Data.Result.builder() + .addAction( + SessionExecuteResponse.Data.Result.Action.builder() + .type("click") + .action("action") + .instruction("instruction") + .pageText("pageText") + .pageUrl("pageUrl") + .reasoning("reasoning") + .taskCompleted(true) + .timeMs(0.0) + .build() + ) + .completed(true) + .message("Successfully logged in and navigated to dashboard") + .success(true) + .metadata( + SessionExecuteResponse.Data.Result.Metadata.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .usage( + SessionExecuteResponse.Data.Result.Usage.builder() + .inferenceTimeMs(2500.0) + .inputTokens(1500.0) + .outputTokens(250.0) + .cachedInputTokens(0.0) + .reasoningTokens(0.0) + .build() + ) + .build() + ) + .cacheEntry( + SessionExecuteResponse.Data.CacheEntry.builder() + .cacheKey("cacheKey") + .entry(JsonValue.from(mapOf())) + .build() + ) + .build() + ) + .success(true) + .build() + + assertThat(sessionExecuteResponse.data()) + .isEqualTo( + SessionExecuteResponse.Data.builder() + .result( + SessionExecuteResponse.Data.Result.builder() + .addAction( + SessionExecuteResponse.Data.Result.Action.builder() + .type("click") + .action("action") + .instruction("instruction") + .pageText("pageText") + .pageUrl("pageUrl") + .reasoning("reasoning") + .taskCompleted(true) + .timeMs(0.0) + .build() + ) + .completed(true) + .message("Successfully logged in and navigated to dashboard") + .success(true) + .metadata( + SessionExecuteResponse.Data.Result.Metadata.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .usage( + SessionExecuteResponse.Data.Result.Usage.builder() + .inferenceTimeMs(2500.0) + .inputTokens(1500.0) + .outputTokens(250.0) + .cachedInputTokens(0.0) + .reasoningTokens(0.0) + .build() + ) + .build() + ) + .cacheEntry( + SessionExecuteResponse.Data.CacheEntry.builder() + .cacheKey("cacheKey") + .entry(JsonValue.from(mapOf())) + .build() + ) + .build() + ) + assertThat(sessionExecuteResponse.success()).isEqualTo(true) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val sessionExecuteResponse = + SessionExecuteResponse.builder() + .data( + SessionExecuteResponse.Data.builder() + .result( + SessionExecuteResponse.Data.Result.builder() + .addAction( + SessionExecuteResponse.Data.Result.Action.builder() + .type("click") + .action("action") + .instruction("instruction") + .pageText("pageText") + .pageUrl("pageUrl") + .reasoning("reasoning") + .taskCompleted(true) + .timeMs(0.0) + .build() + ) + .completed(true) + .message("Successfully logged in and navigated to dashboard") + .success(true) + .metadata( + SessionExecuteResponse.Data.Result.Metadata.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .usage( + SessionExecuteResponse.Data.Result.Usage.builder() + .inferenceTimeMs(2500.0) + .inputTokens(1500.0) + .outputTokens(250.0) + .cachedInputTokens(0.0) + .reasoningTokens(0.0) + .build() + ) + .build() + ) + .cacheEntry( + SessionExecuteResponse.Data.CacheEntry.builder() + .cacheKey("cacheKey") + .entry(JsonValue.from(mapOf())) + .build() + ) + .build() + ) + .success(true) + .build() + + val roundtrippedSessionExecuteResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(sessionExecuteResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSessionExecuteResponse).isEqualTo(sessionExecuteResponse) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractParamsTest.kt new file mode 100644 index 0000000..30d7652 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractParamsTest.kt @@ -0,0 +1,183 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.http.Headers +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionExtractParamsTest { + + @Test + fun create() { + SessionExtractParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Extract all product names and prices from the page") + .options( + SessionExtractParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("#main-content") + .timeout(30000.0) + .build() + ) + .schema( + SessionExtractParams.Schema.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + } + + @Test + fun pathParams() { + val params = + SessionExtractParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + assertThat(params._pathParam(0)).isEqualTo("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + // out-of-bound path param + assertThat(params._pathParam(1)).isEqualTo("") + } + + @Test + fun headers() { + val params = + SessionExtractParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Extract all product names and prices from the page") + .options( + SessionExtractParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("#main-content") + .timeout(30000.0) + .build() + ) + .schema( + SessionExtractParams.Schema.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) + } + + @Test + fun headersWithoutOptionalFields() { + val params = + SessionExtractParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().build()) + } + + @Test + fun body() { + val params = + SessionExtractParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Extract all product names and prices from the page") + .options( + SessionExtractParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("#main-content") + .timeout(30000.0) + .build() + ) + .schema( + SessionExtractParams.Schema.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + + val body = params._body() + + assertThat(body.frameId()).contains("frameId") + assertThat(body.instruction()) + .contains("Extract all product names and prices from the page") + assertThat(body.options()) + .contains( + SessionExtractParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("#main-content") + .timeout(30000.0) + .build() + ) + assertThat(body.schema()) + .contains( + SessionExtractParams.Schema.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + } + + @Test + fun bodyWithoutOptionalFields() { + val params = + SessionExtractParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + val body = params._body() + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractResponseTest.kt new file mode 100644 index 0000000..3f055cd --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractResponseTest.kt @@ -0,0 +1,58 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionExtractResponseTest { + + @Test + fun create() { + val sessionExtractResponse = + SessionExtractResponse.builder() + .data( + SessionExtractResponse.Data.builder() + .result(JsonValue.from(mapOf())) + .actionId("actionId") + .build() + ) + .success(true) + .build() + + assertThat(sessionExtractResponse.data()) + .isEqualTo( + SessionExtractResponse.Data.builder() + .result(JsonValue.from(mapOf())) + .actionId("actionId") + .build() + ) + assertThat(sessionExtractResponse.success()).isEqualTo(true) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val sessionExtractResponse = + SessionExtractResponse.builder() + .data( + SessionExtractResponse.Data.builder() + .result(JsonValue.from(mapOf())) + .actionId("actionId") + .build() + ) + .success(true) + .build() + + val roundtrippedSessionExtractResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(sessionExtractResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSessionExtractResponse).isEqualTo(sessionExtractResponse) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateParamsTest.kt new file mode 100644 index 0000000..3e48bb2 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateParamsTest.kt @@ -0,0 +1,123 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.http.Headers +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionNavigateParamsTest { + + @Test + fun create() { + SessionNavigateParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) + .url("https://example.com") + .frameId("frameId") + .options( + SessionNavigateParams.Options.builder() + .referer("referer") + .timeout(30000.0) + .waitUntil(SessionNavigateParams.Options.WaitUntil.NETWORKIDLE) + .build() + ) + .streamResponse(true) + .build() + } + + @Test + fun pathParams() { + val params = + SessionNavigateParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .url("https://example.com") + .build() + + assertThat(params._pathParam(0)).isEqualTo("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + // out-of-bound path param + assertThat(params._pathParam(1)).isEqualTo("") + } + + @Test + fun headers() { + val params = + SessionNavigateParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) + .url("https://example.com") + .frameId("frameId") + .options( + SessionNavigateParams.Options.builder() + .referer("referer") + .timeout(30000.0) + .waitUntil(SessionNavigateParams.Options.WaitUntil.NETWORKIDLE) + .build() + ) + .streamResponse(true) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) + } + + @Test + fun headersWithoutOptionalFields() { + val params = + SessionNavigateParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .url("https://example.com") + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().build()) + } + + @Test + fun body() { + val params = + SessionNavigateParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) + .url("https://example.com") + .frameId("frameId") + .options( + SessionNavigateParams.Options.builder() + .referer("referer") + .timeout(30000.0) + .waitUntil(SessionNavigateParams.Options.WaitUntil.NETWORKIDLE) + .build() + ) + .streamResponse(true) + .build() + + val body = params._body() + + assertThat(body.url()).isEqualTo("https://example.com") + assertThat(body.frameId()).contains("frameId") + assertThat(body.options()) + .contains( + SessionNavigateParams.Options.builder() + .referer("referer") + .timeout(30000.0) + .waitUntil(SessionNavigateParams.Options.WaitUntil.NETWORKIDLE) + .build() + ) + assertThat(body.streamResponse()).contains(true) + } + + @Test + fun bodyWithoutOptionalFields() { + val params = + SessionNavigateParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .url("https://example.com") + .build() + + val body = params._body() + + assertThat(body.url()).isEqualTo("https://example.com") + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponseTest.kt new file mode 100644 index 0000000..bca29d9 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponseTest.kt @@ -0,0 +1,58 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionNavigateResponseTest { + + @Test + fun create() { + val sessionNavigateResponse = + SessionNavigateResponse.builder() + .data( + SessionNavigateResponse.Data.builder() + .result(JsonValue.from(mapOf())) + .actionId("actionId") + .build() + ) + .success(true) + .build() + + assertThat(sessionNavigateResponse.data()) + .isEqualTo( + SessionNavigateResponse.Data.builder() + .result(JsonValue.from(mapOf())) + .actionId("actionId") + .build() + ) + assertThat(sessionNavigateResponse.success()).isEqualTo(true) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val sessionNavigateResponse = + SessionNavigateResponse.builder() + .data( + SessionNavigateResponse.Data.builder() + .result(JsonValue.from(mapOf())) + .actionId("actionId") + .build() + ) + .success(true) + .build() + + val roundtrippedSessionNavigateResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(sessionNavigateResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSessionNavigateResponse).isEqualTo(sessionNavigateResponse) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveParamsTest.kt new file mode 100644 index 0000000..df6c3f7 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveParamsTest.kt @@ -0,0 +1,217 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.http.Headers +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionObserveParamsTest { + + @Test + fun create() { + SessionObserveParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Find all clickable navigation links") + .options( + SessionObserveParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("nav") + .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) + .build() + ) + .build() + } + + @Test + fun pathParams() { + val params = + SessionObserveParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + assertThat(params._pathParam(0)).isEqualTo("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + // out-of-bound path param + assertThat(params._pathParam(1)).isEqualTo("") + } + + @Test + fun headers() { + val params = + SessionObserveParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Find all clickable navigation links") + .options( + SessionObserveParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("nav") + .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) + .build() + ) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) + } + + @Test + fun headersWithoutOptionalFields() { + val params = + SessionObserveParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().build()) + } + + @Test + fun body() { + val params = + SessionObserveParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Find all clickable navigation links") + .options( + SessionObserveParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("nav") + .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) + .build() + ) + .build() + + val body = params._body() + + assertThat(body.frameId()).contains("frameId") + assertThat(body.instruction()).contains("Find all clickable navigation links") + assertThat(body.options()) + .contains( + SessionObserveParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("nav") + .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) + .build() + ) + } + + @Test + fun bodyWithoutOptionalFields() { + val params = + SessionObserveParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + val body = params._body() + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveResponseTest.kt new file mode 100644 index 0000000..938fff3 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveResponseTest.kt @@ -0,0 +1,81 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionObserveResponseTest { + + @Test + fun create() { + val sessionObserveResponse = + SessionObserveResponse.builder() + .data( + SessionObserveResponse.Data.builder() + .addResult( + SessionObserveResponse.Data.Result.builder() + .description("Click the submit button") + .selector("[data-testid='submit-button']") + .addArgument("Hello World") + .backendNodeId(0.0) + .method("click") + .build() + ) + .actionId("actionId") + .build() + ) + .success(true) + .build() + + assertThat(sessionObserveResponse.data()) + .isEqualTo( + SessionObserveResponse.Data.builder() + .addResult( + SessionObserveResponse.Data.Result.builder() + .description("Click the submit button") + .selector("[data-testid='submit-button']") + .addArgument("Hello World") + .backendNodeId(0.0) + .method("click") + .build() + ) + .actionId("actionId") + .build() + ) + assertThat(sessionObserveResponse.success()).isEqualTo(true) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val sessionObserveResponse = + SessionObserveResponse.builder() + .data( + SessionObserveResponse.Data.builder() + .addResult( + SessionObserveResponse.Data.Result.builder() + .description("Click the submit button") + .selector("[data-testid='submit-button']") + .addArgument("Hello World") + .backendNodeId(0.0) + .method("click") + .build() + ) + .actionId("actionId") + .build() + ) + .success(true) + .build() + + val roundtrippedSessionObserveResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(sessionObserveResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSessionObserveResponse).isEqualTo(sessionObserveResponse) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayParamsTest.kt new file mode 100644 index 0000000..7bef35a --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayParamsTest.kt @@ -0,0 +1,51 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.http.Headers +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionReplayParamsTest { + + @Test + fun create() { + SessionReplayParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionReplayParams.XStreamResponse.TRUE) + .build() + } + + @Test + fun pathParams() { + val params = + SessionReplayParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + assertThat(params._pathParam(0)).isEqualTo("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + // out-of-bound path param + assertThat(params._pathParam(1)).isEqualTo("") + } + + @Test + fun headers() { + val params = + SessionReplayParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionReplayParams.XStreamResponse.TRUE) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) + } + + @Test + fun headersWithoutOptionalFields() { + val params = + SessionReplayParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().build()) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayResponseTest.kt new file mode 100644 index 0000000..0c0e456 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayResponseTest.kt @@ -0,0 +1,155 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionReplayResponseTest { + + @Test + fun create() { + val sessionReplayResponse = + SessionReplayResponse.builder() + .data( + SessionReplayResponse.Data.builder() + .addPage( + SessionReplayResponse.Data.Page.builder() + .addAction( + SessionReplayResponse.Data.Page.Action.builder() + .method("method") + .parameters( + SessionReplayResponse.Data.Page.Action.Parameters + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .result( + SessionReplayResponse.Data.Page.Action.Result.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .timestamp(0.0) + .endTime(0.0) + .tokenUsage( + SessionReplayResponse.Data.Page.Action.TokenUsage + .builder() + .cost(0.0) + .inputTokens(0.0) + .outputTokens(0.0) + .timeMs(0.0) + .build() + ) + .build() + ) + .duration(0.0) + .timestamp(0.0) + .url("url") + .build() + ) + .clientLanguage("clientLanguage") + .build() + ) + .success(true) + .build() + + assertThat(sessionReplayResponse.data()) + .isEqualTo( + SessionReplayResponse.Data.builder() + .addPage( + SessionReplayResponse.Data.Page.builder() + .addAction( + SessionReplayResponse.Data.Page.Action.builder() + .method("method") + .parameters( + SessionReplayResponse.Data.Page.Action.Parameters.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .result( + SessionReplayResponse.Data.Page.Action.Result.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .timestamp(0.0) + .endTime(0.0) + .tokenUsage( + SessionReplayResponse.Data.Page.Action.TokenUsage.builder() + .cost(0.0) + .inputTokens(0.0) + .outputTokens(0.0) + .timeMs(0.0) + .build() + ) + .build() + ) + .duration(0.0) + .timestamp(0.0) + .url("url") + .build() + ) + .clientLanguage("clientLanguage") + .build() + ) + assertThat(sessionReplayResponse.success()).isEqualTo(true) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val sessionReplayResponse = + SessionReplayResponse.builder() + .data( + SessionReplayResponse.Data.builder() + .addPage( + SessionReplayResponse.Data.Page.builder() + .addAction( + SessionReplayResponse.Data.Page.Action.builder() + .method("method") + .parameters( + SessionReplayResponse.Data.Page.Action.Parameters + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .result( + SessionReplayResponse.Data.Page.Action.Result.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .timestamp(0.0) + .endTime(0.0) + .tokenUsage( + SessionReplayResponse.Data.Page.Action.TokenUsage + .builder() + .cost(0.0) + .inputTokens(0.0) + .outputTokens(0.0) + .timeMs(0.0) + .build() + ) + .build() + ) + .duration(0.0) + .timestamp(0.0) + .url("url") + .build() + ) + .clientLanguage("clientLanguage") + .build() + ) + .success(true) + .build() + + val roundtrippedSessionReplayResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(sessionReplayResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSessionReplayResponse).isEqualTo(sessionReplayResponse) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartParamsTest.kt new file mode 100644 index 0000000..0a8dffc --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartParamsTest.kt @@ -0,0 +1,673 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.http.Headers +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionStartParamsTest { + + @Test + fun create() { + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport.builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings.builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings.Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region(SessionStartParams.BrowserbaseSessionCreateParams.Region.US_WEST_2) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + } + + @Test + fun headers() { + val params = + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport.builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region(SessionStartParams.BrowserbaseSessionCreateParams.Region.US_WEST_2) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) + } + + @Test + fun headersWithoutOptionalFields() { + val params = SessionStartParams.builder().modelName("openai/gpt-5.4-mini").build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().build()) + } + + @Test + fun body() { + val params = + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport.builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region(SessionStartParams.BrowserbaseSessionCreateParams.Region.US_WEST_2) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + + val body = params._body() + + assertThat(body.modelName()).isEqualTo("openai/gpt-5.4-mini") + assertThat(body.actTimeoutMs()).contains(0.0) + assertThat(body.browser()) + .contains( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport.builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + assertThat(body.browserbaseSessionCreateParams()) + .contains( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings.builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings.Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region(SessionStartParams.BrowserbaseSessionCreateParams.Region.US_WEST_2) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + assertThat(body.browserbaseSessionId()).contains("browserbaseSessionID") + assertThat(body.domSettleTimeoutMs()).contains(5000.0) + assertThat(body.experimental()).contains(true) + assertThat(body.selfHeal()).contains(true) + assertThat(body.systemPrompt()).contains("systemPrompt") + assertThat(body.verbose()).contains(SessionStartParams.Verbose._1) + assertThat(body.waitForCaptchaSolves()).contains(true) + } + + @Test + fun bodyWithoutOptionalFields() { + val params = SessionStartParams.builder().modelName("openai/gpt-5.4-mini").build() + + val body = params._body() + + assertThat(body.modelName()).isEqualTo("openai/gpt-5.4-mini") + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartResponseTest.kt new file mode 100644 index 0000000..0ae6883 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartResponseTest.kt @@ -0,0 +1,60 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionStartResponseTest { + + @Test + fun create() { + val sessionStartResponse = + SessionStartResponse.builder() + .data( + SessionStartResponse.Data.builder() + .available(true) + .sessionId("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .cdpUrl("wss://connect.browserbase.com/?signingKey=abc123") + .build() + ) + .success(true) + .build() + + assertThat(sessionStartResponse.data()) + .isEqualTo( + SessionStartResponse.Data.builder() + .available(true) + .sessionId("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .cdpUrl("wss://connect.browserbase.com/?signingKey=abc123") + .build() + ) + assertThat(sessionStartResponse.success()).isEqualTo(true) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val sessionStartResponse = + SessionStartResponse.builder() + .data( + SessionStartResponse.Data.builder() + .available(true) + .sessionId("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .cdpUrl("wss://connect.browserbase.com/?signingKey=abc123") + .build() + ) + .success(true) + .build() + + val roundtrippedSessionStartResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(sessionStartResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSessionStartResponse).isEqualTo(sessionStartResponse) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/StreamEventTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/StreamEventTest.kt new file mode 100644 index 0000000..9d58bf8 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/StreamEventTest.kt @@ -0,0 +1,66 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class StreamEventTest { + + @Test + fun create() { + val streamEvent = + StreamEvent.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .data( + StreamEvent.Data.StreamEventSystemDataOutput.builder() + .status(StreamEvent.Data.StreamEventSystemDataOutput.Status.STARTING) + .error("error") + .result(JsonValue.from(mapOf())) + .build() + ) + .type(StreamEvent.Type.SYSTEM) + .build() + + assertThat(streamEvent.id()).isEqualTo("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + assertThat(streamEvent.data()) + .isEqualTo( + StreamEvent.Data.ofStreamEventSystemDataOutput( + StreamEvent.Data.StreamEventSystemDataOutput.builder() + .status(StreamEvent.Data.StreamEventSystemDataOutput.Status.STARTING) + .error("error") + .result(JsonValue.from(mapOf())) + .build() + ) + ) + assertThat(streamEvent.type()).isEqualTo(StreamEvent.Type.SYSTEM) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val streamEvent = + StreamEvent.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .data( + StreamEvent.Data.StreamEventSystemDataOutput.builder() + .status(StreamEvent.Data.StreamEventSystemDataOutput.Status.STARTING) + .error("error") + .result(JsonValue.from(mapOf())) + .build() + ) + .type(StreamEvent.Type.SYSTEM) + .build() + + val roundtrippedStreamEvent = + jsonMapper.readValue( + jsonMapper.writeValueAsString(streamEvent), + jacksonTypeRef(), + ) + + assertThat(roundtrippedStreamEvent).isEqualTo(streamEvent) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ErrorHandlingTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ErrorHandlingTest.kt new file mode 100644 index 0000000..6941c36 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ErrorHandlingTest.kt @@ -0,0 +1,3378 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.services + +import com.browserbase.api.client.StagehandClient +import com.browserbase.api.client.okhttp.StagehandOkHttpClient +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.jsonMapper +import com.browserbase.api.errors.BadRequestException +import com.browserbase.api.errors.InternalServerException +import com.browserbase.api.errors.NotFoundException +import com.browserbase.api.errors.PermissionDeniedException +import com.browserbase.api.errors.RateLimitException +import com.browserbase.api.errors.StagehandException +import com.browserbase.api.errors.UnauthorizedException +import com.browserbase.api.errors.UnexpectedStatusCodeException +import com.browserbase.api.errors.UnprocessableEntityException +import com.browserbase.api.models.sessions.SessionStartParams +import com.github.tomakehurst.wiremock.client.WireMock.anyUrl +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.status +import com.github.tomakehurst.wiremock.client.WireMock.stubFor +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo +import com.github.tomakehurst.wiremock.junit5.WireMockTest +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.entry +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.parallel.ResourceLock + +@WireMockTest +@ResourceLock("https://github.com/wiremock/wiremock/issues/169") +internal class ErrorHandlingTest { + + companion object { + + private val ERROR_JSON: JsonValue = JsonValue.from(mapOf("errorProperty" to "42")) + + private val ERROR_JSON_BYTES: ByteArray = jsonMapper().writeValueAsBytes(ERROR_JSON) + + private const val HEADER_NAME: String = "Error-Header" + + private const val HEADER_VALUE: String = "42" + + private const val NOT_JSON: String = "Not JSON" + } + + private lateinit var client: StagehandClient + + @BeforeEach + fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) { + client = + StagehandOkHttpClient.builder() + .baseUrl(wmRuntimeInfo.httpBaseUrl) + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + } + + @Test + fun sessionsStart400() { + val sessionService = client.sessions() + stubFor( + post(anyUrl()) + .willReturn( + status(400).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(400) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart400WithRawResponse() { + val sessionService = client.sessions().withRawResponse() + stubFor( + post(anyUrl()) + .willReturn( + status(400).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(400) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart401() { + val sessionService = client.sessions() + stubFor( + post(anyUrl()) + .willReturn( + status(401).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(401) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart401WithRawResponse() { + val sessionService = client.sessions().withRawResponse() + stubFor( + post(anyUrl()) + .willReturn( + status(401).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(401) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart403() { + val sessionService = client.sessions() + stubFor( + post(anyUrl()) + .willReturn( + status(403).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(403) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart403WithRawResponse() { + val sessionService = client.sessions().withRawResponse() + stubFor( + post(anyUrl()) + .willReturn( + status(403).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(403) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart404() { + val sessionService = client.sessions() + stubFor( + post(anyUrl()) + .willReturn( + status(404).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(404) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart404WithRawResponse() { + val sessionService = client.sessions().withRawResponse() + stubFor( + post(anyUrl()) + .willReturn( + status(404).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(404) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart422() { + val sessionService = client.sessions() + stubFor( + post(anyUrl()) + .willReturn( + status(422).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(422) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart422WithRawResponse() { + val sessionService = client.sessions().withRawResponse() + stubFor( + post(anyUrl()) + .willReturn( + status(422).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(422) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart429() { + val sessionService = client.sessions() + stubFor( + post(anyUrl()) + .willReturn( + status(429).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(429) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart429WithRawResponse() { + val sessionService = client.sessions().withRawResponse() + stubFor( + post(anyUrl()) + .willReturn( + status(429).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(429) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart500() { + val sessionService = client.sessions() + stubFor( + post(anyUrl()) + .willReturn( + status(500).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(500) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart500WithRawResponse() { + val sessionService = client.sessions().withRawResponse() + stubFor( + post(anyUrl()) + .willReturn( + status(500).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(500) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart999() { + val sessionService = client.sessions() + stubFor( + post(anyUrl()) + .willReturn( + status(999).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(999) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStart999WithRawResponse() { + val sessionService = client.sessions().withRawResponse() + stubFor( + post(anyUrl()) + .willReturn( + status(999).withHeader(HEADER_NAME, HEADER_VALUE).withBody(ERROR_JSON_BYTES) + ) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e.statusCode()).isEqualTo(999) + assertThat(e.headers().toMap()).contains(entry(HEADER_NAME, listOf(HEADER_VALUE))) + assertThat(e.body()).isEqualTo(ERROR_JSON) + } + + @Test + fun sessionsStartInvalidJsonBody() { + val sessionService = client.sessions() + stubFor( + post(anyUrl()) + .willReturn(status(200).withHeader(HEADER_NAME, HEADER_VALUE).withBody(NOT_JSON)) + ) + + val e = + assertThrows { + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams + .BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region + .US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + } + + assertThat(e).hasMessage("Error reading response") + } + + private fun Headers.toMap(): Map> = + mutableMapOf>().also { map -> + names().forEach { map[it] = values(it) } + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ServiceParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ServiceParamsTest.kt new file mode 100644 index 0000000..392cc05 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ServiceParamsTest.kt @@ -0,0 +1,279 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.services + +import com.browserbase.api.client.StagehandClient +import com.browserbase.api.client.okhttp.StagehandOkHttpClient +import com.browserbase.api.core.JsonValue +import com.browserbase.api.models.sessions.ModelConfig +import com.browserbase.api.models.sessions.SessionActParams +import com.browserbase.api.models.sessions.SessionStartParams +import com.github.tomakehurst.wiremock.client.WireMock.anyUrl +import com.github.tomakehurst.wiremock.client.WireMock.equalTo +import com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath +import com.github.tomakehurst.wiremock.client.WireMock.ok +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor +import com.github.tomakehurst.wiremock.client.WireMock.stubFor +import com.github.tomakehurst.wiremock.client.WireMock.verify +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo +import com.github.tomakehurst.wiremock.junit5.WireMockTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.ResourceLock + +@WireMockTest +@ResourceLock("https://github.com/wiremock/wiremock/issues/169") +internal class ServiceParamsTest { + + private lateinit var client: StagehandClient + + @BeforeEach + fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) { + client = + StagehandOkHttpClient.builder() + .baseUrl(wmRuntimeInfo.httpBaseUrl) + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + } + + @Disabled("Mock server tests are disabled") + @Test + fun start() { + val sessionService = client.sessions() + stubFor(post(anyUrl()).willReturn(ok("{}"))) + + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport.builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region(SessionStartParams.BrowserbaseSessionCreateParams.Region.US_WEST_2) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .putAdditionalHeader("Secret-Header", "42") + .putAdditionalQueryParam("secret_query_param", "42") + .putAdditionalBodyProperty("secretProperty", JsonValue.from("42")) + .build() + ) + + verify( + postRequestedFor(anyUrl()) + .withHeader("Secret-Header", equalTo("42")) + .withQueryParam("secret_query_param", equalTo("42")) + .withRequestBody(matchingJsonPath("$.secretProperty", equalTo("42"))) + ) + } + + @Disabled("Mock server tests are disabled") + @Test + fun act() { + val sessionService = client.sessions() + stubFor(post(anyUrl()).willReturn(ok("{}"))) + + sessionService.act( + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionActParams.XStreamResponse.TRUE) + .input("Click the login button") + .frameId("frameId") + .options( + SessionActParams.Options.builder() + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .timeout(30000.0) + .variables( + SessionActParams.Options.Variables.builder() + .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) + .build() + ) + .build() + ) + .putAdditionalHeader("Secret-Header", "42") + .putAdditionalQueryParam("secret_query_param", "42") + .putAdditionalBodyProperty("secretProperty", JsonValue.from("42")) + .build() + ) + + verify( + postRequestedFor(anyUrl()) + .withHeader("Secret-Header", equalTo("42")) + .withQueryParam("secret_query_param", equalTo("42")) + .withRequestBody(matchingJsonPath("$.secretProperty", equalTo("42"))) + ) + } +} 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 new file mode 100644 index 0000000..369528c --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/async/SessionServiceAsyncTest.kt @@ -0,0 +1,767 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.services.async + +import com.browserbase.api.client.okhttp.StagehandOkHttpClientAsync +import com.browserbase.api.core.JsonValue +import com.browserbase.api.models.sessions.ModelConfig +import com.browserbase.api.models.sessions.SessionActParams +import com.browserbase.api.models.sessions.SessionEndParams +import com.browserbase.api.models.sessions.SessionExecuteParams +import com.browserbase.api.models.sessions.SessionExtractParams +import com.browserbase.api.models.sessions.SessionNavigateParams +import com.browserbase.api.models.sessions.SessionObserveParams +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionStartParams +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +internal class SessionServiceAsyncTest { + + @Disabled("Mock server tests are disabled") + @Test + fun act() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseFuture = + sessionServiceAsync.act( + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionActParams.XStreamResponse.TRUE) + .input("Click the login button") + .frameId("frameId") + .options( + SessionActParams.Options.builder() + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .timeout(30000.0) + .variables( + SessionActParams.Options.Variables.builder() + .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) + .build() + ) + .build() + ) + .build() + ) + + val response = responseFuture.get() + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun actStreaming() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseStreamResponse = + sessionServiceAsync.actStreaming( + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionActParams.XStreamResponse.TRUE) + .input("Click the login button") + .frameId("frameId") + .options( + SessionActParams.Options.builder() + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .timeout(30000.0) + .variables( + SessionActParams.Options.Variables.builder() + .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) + .build() + ) + .build() + ) + .build() + ) + + val onCompleteFuture = + responseStreamResponse.subscribe { response -> response.validate() }.onCompleteFuture() + onCompleteFuture.get() + } + + @Disabled("Mock server tests are disabled") + @Test + fun end() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseFuture = + sessionServiceAsync.end( + SessionEndParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) + .build() + ) + + val response = responseFuture.get() + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun execute() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseFuture = + sessionServiceAsync.execute( + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) + .agentConfig( + SessionExecuteParams.AgentConfig.builder() + .cua(true) + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) + .systemPrompt("systemPrompt") + .build() + ) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .highlightCursor(true) + .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) + .variables( + SessionExecuteParams.ExecuteOptions.Variables.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .build() + ) + .frameId("frameId") + .shouldCache(true) + .build() + ) + + val response = responseFuture.get() + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun executeStreaming() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseStreamResponse = + sessionServiceAsync.executeStreaming( + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) + .agentConfig( + SessionExecuteParams.AgentConfig.builder() + .cua(true) + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) + .systemPrompt("systemPrompt") + .build() + ) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .highlightCursor(true) + .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) + .variables( + SessionExecuteParams.ExecuteOptions.Variables.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .build() + ) + .frameId("frameId") + .shouldCache(true) + .build() + ) + + val onCompleteFuture = + responseStreamResponse.subscribe { response -> response.validate() }.onCompleteFuture() + onCompleteFuture.get() + } + + @Disabled("Mock server tests are disabled") + @Test + fun extract() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseFuture = + sessionServiceAsync.extract( + SessionExtractParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Extract all product names and prices from the page") + .options( + SessionExtractParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("#main-content") + .timeout(30000.0) + .build() + ) + .schema( + SessionExtractParams.Schema.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + + val response = responseFuture.get() + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun extractStreaming() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseStreamResponse = + sessionServiceAsync.extractStreaming( + SessionExtractParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Extract all product names and prices from the page") + .options( + SessionExtractParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("#main-content") + .timeout(30000.0) + .build() + ) + .schema( + SessionExtractParams.Schema.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + + val onCompleteFuture = + responseStreamResponse.subscribe { response -> response.validate() }.onCompleteFuture() + onCompleteFuture.get() + } + + @Disabled("Mock server tests are disabled") + @Test + fun navigate() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseFuture = + sessionServiceAsync.navigate( + SessionNavigateParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) + .url("https://example.com") + .frameId("frameId") + .options( + SessionNavigateParams.Options.builder() + .referer("referer") + .timeout(30000.0) + .waitUntil(SessionNavigateParams.Options.WaitUntil.NETWORKIDLE) + .build() + ) + .streamResponse(true) + .build() + ) + + val response = responseFuture.get() + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun observe() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseFuture = + sessionServiceAsync.observe( + SessionObserveParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Find all clickable navigation links") + .options( + SessionObserveParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("nav") + .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) + .build() + ) + .build() + ) + + val response = responseFuture.get() + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun observeStreaming() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseStreamResponse = + sessionServiceAsync.observeStreaming( + SessionObserveParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Find all clickable navigation links") + .options( + SessionObserveParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("nav") + .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) + .build() + ) + .build() + ) + + val onCompleteFuture = + responseStreamResponse.subscribe { response -> response.validate() }.onCompleteFuture() + onCompleteFuture.get() + } + + @Disabled("Mock server tests are disabled") + @Test + fun replay() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseFuture = + sessionServiceAsync.replay( + SessionReplayParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionReplayParams.XStreamResponse.TRUE) + .build() + ) + + val response = responseFuture.get() + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun start() { + val client = + StagehandOkHttpClientAsync.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseFuture = + sessionServiceAsync.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport.builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region.US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + + val response = responseFuture.get() + response.validate() + } +} 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 new file mode 100644 index 0000000..2cea0fd --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/blocking/SessionServiceTest.kt @@ -0,0 +1,759 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.services.blocking + +import com.browserbase.api.client.okhttp.StagehandOkHttpClient +import com.browserbase.api.core.JsonValue +import com.browserbase.api.models.sessions.ModelConfig +import com.browserbase.api.models.sessions.SessionActParams +import com.browserbase.api.models.sessions.SessionEndParams +import com.browserbase.api.models.sessions.SessionExecuteParams +import com.browserbase.api.models.sessions.SessionExtractParams +import com.browserbase.api.models.sessions.SessionNavigateParams +import com.browserbase.api.models.sessions.SessionObserveParams +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionStartParams +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +internal class SessionServiceTest { + + @Disabled("Mock server tests are disabled") + @Test + fun act() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val response = + sessionService.act( + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionActParams.XStreamResponse.TRUE) + .input("Click the login button") + .frameId("frameId") + .options( + SessionActParams.Options.builder() + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .timeout(30000.0) + .variables( + SessionActParams.Options.Variables.builder() + .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) + .build() + ) + .build() + ) + .build() + ) + + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun actStreaming() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val responseStreamResponse = + sessionService.actStreaming( + SessionActParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionActParams.XStreamResponse.TRUE) + .input("Click the login button") + .frameId("frameId") + .options( + SessionActParams.Options.builder() + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .timeout(30000.0) + .variables( + SessionActParams.Options.Variables.builder() + .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) + .build() + ) + .build() + ) + .build() + ) + + responseStreamResponse.use { + responseStreamResponse.stream().forEach { response -> response.validate() } + } + } + + @Disabled("Mock server tests are disabled") + @Test + fun end() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val response = + sessionService.end( + SessionEndParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) + .build() + ) + + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun execute() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val response = + sessionService.execute( + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) + .agentConfig( + SessionExecuteParams.AgentConfig.builder() + .cua(true) + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) + .systemPrompt("systemPrompt") + .build() + ) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .highlightCursor(true) + .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) + .variables( + SessionExecuteParams.ExecuteOptions.Variables.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .build() + ) + .frameId("frameId") + .shouldCache(true) + .build() + ) + + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun executeStreaming() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val responseStreamResponse = + sessionService.executeStreaming( + SessionExecuteParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) + .agentConfig( + SessionExecuteParams.AgentConfig.builder() + .cua(true) + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) + .systemPrompt("systemPrompt") + .build() + ) + .executeOptions( + SessionExecuteParams.ExecuteOptions.builder() + .instruction( + "Log in with username 'demo' and password 'test123', then navigate to settings" + ) + .highlightCursor(true) + .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) + .variables( + SessionExecuteParams.ExecuteOptions.Variables.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .build() + ) + .frameId("frameId") + .shouldCache(true) + .build() + ) + + responseStreamResponse.use { + responseStreamResponse.stream().forEach { response -> response.validate() } + } + } + + @Disabled("Mock server tests are disabled") + @Test + fun extract() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val response = + sessionService.extract( + SessionExtractParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Extract all product names and prices from the page") + .options( + SessionExtractParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("#main-content") + .timeout(30000.0) + .build() + ) + .schema( + SessionExtractParams.Schema.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun extractStreaming() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val responseStreamResponse = + sessionService.extractStreaming( + SessionExtractParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Extract all product names and prices from the page") + .options( + SessionExtractParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("#main-content") + .timeout(30000.0) + .build() + ) + .schema( + SessionExtractParams.Schema.builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + + responseStreamResponse.use { + responseStreamResponse.stream().forEach { response -> response.validate() } + } + } + + @Disabled("Mock server tests are disabled") + @Test + fun navigate() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val response = + sessionService.navigate( + SessionNavigateParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) + .url("https://example.com") + .frameId("frameId") + .options( + SessionNavigateParams.Options.builder() + .referer("referer") + .timeout(30000.0) + .waitUntil(SessionNavigateParams.Options.WaitUntil.NETWORKIDLE) + .build() + ) + .streamResponse(true) + .build() + ) + + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun observe() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val response = + sessionService.observe( + SessionObserveParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Find all clickable navigation links") + .options( + SessionObserveParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("nav") + .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) + .build() + ) + .build() + ) + + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun observeStreaming() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val responseStreamResponse = + sessionService.observeStreaming( + SessionObserveParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) + .frameId("frameId") + .instruction("Find all clickable navigation links") + .options( + SessionObserveParams.Options.builder() + .ignoreSelectors(listOf("nav", ".cookie-banner", "#sidebar-ads")) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5.4-mini") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .selector("nav") + .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) + .build() + ) + .build() + ) + + responseStreamResponse.use { + responseStreamResponse.stream().forEach { response -> response.validate() } + } + } + + @Disabled("Mock server tests are disabled") + @Test + fun replay() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val response = + sessionService.replay( + SessionReplayParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionReplayParams.XStreamResponse.TRUE) + .build() + ) + + response.validate() + } + + @Disabled("Mock server tests are disabled") + @Test + fun start() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val response = + sessionService.start( + SessionStartParams.builder() + .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) + .modelName("openai/gpt-5.4-mini") + .actTimeoutMs(0.0) + .browser( + SessionStartParams.Browser.builder() + .cdpUrl("ws://localhost:9222") + .launchOptions( + SessionStartParams.Browser.LaunchOptions.builder() + .acceptDownloads(true) + .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) + .cdpUrl("cdpUrl") + .chromiumSandbox(true) + .connectTimeoutMs(0.0) + .deviceScaleFactor(0.0) + .devtools(true) + .downloadsPath("downloadsPath") + .executablePath("executablePath") + .hasTouch(true) + .headless(true) + .ignoreDefaultArgs(true) + .ignoreHttpsErrors(true) + .locale("locale") + .port(0.0) + .preserveUserDataDir(true) + .proxy( + SessionStartParams.Browser.LaunchOptions.Proxy.builder() + .server("server") + .bypass("bypass") + .password("password") + .username("username") + .build() + ) + .userDataDir("userDataDir") + .viewport( + SessionStartParams.Browser.LaunchOptions.Viewport.builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .type(SessionStartParams.Browser.Type.LOCAL) + .build() + ) + .browserbaseSessionCreateParams( + SessionStartParams.BrowserbaseSessionCreateParams.builder() + .browserSettings( + SessionStartParams.BrowserbaseSessionCreateParams.BrowserSettings + .builder() + .advancedStealth(true) + .blockAds(true) + .captchaImageSelector("captchaImageSelector") + .captchaInputSelector("captchaInputSelector") + .context( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Context + .builder() + .id("id") + .persist(true) + .build() + ) + .extensionId("extensionId") + .fingerprint( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .builder() + .addBrowser( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Browser + .CHROME + ) + .addDevice( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Device + .DESKTOP + ) + .httpVersion( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .HttpVersion + ._1 + ) + .addLocale("string") + .addOperatingSystem( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .OperatingSystem + .ANDROID + ) + .screen( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Fingerprint + .Screen + .builder() + .maxHeight(0.0) + .maxWidth(0.0) + .minHeight(0.0) + .minWidth(0.0) + .build() + ) + .build() + ) + .logSession(true) + .os( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Os + .WINDOWS + ) + .recordSession(true) + .solveCaptchas(true) + .verified(true) + .viewport( + SessionStartParams.BrowserbaseSessionCreateParams + .BrowserSettings + .Viewport + .builder() + .height(0.0) + .width(0.0) + .build() + ) + .build() + ) + .extensionId("extensionId") + .keepAlive(true) + .projectId("projectId") + .proxies(true) + .region( + SessionStartParams.BrowserbaseSessionCreateParams.Region.US_WEST_2 + ) + .timeout(0.0) + .userMetadata( + SessionStartParams.BrowserbaseSessionCreateParams.UserMetadata + .builder() + .putAdditionalProperty("foo", JsonValue.from("bar")) + .build() + ) + .build() + ) + .browserbaseSessionId("browserbaseSessionID") + .domSettleTimeoutMs(5000.0) + .experimental(true) + .selfHeal(true) + .systemPrompt("systemPrompt") + .verbose(SessionStartParams.Verbose._1) + .waitForCaptchaSolves(true) + .build() + ) + + response.validate() + } +} diff --git a/stagehand-java-example/build.gradle.kts b/stagehand-java-example/build.gradle.kts new file mode 100644 index 0000000..baeb3f7 --- /dev/null +++ b/stagehand-java-example/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("stagehand.java") + application +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":stagehand-java")) +} + +tasks.withType().configureEach { + // Allow using more modern APIs, like `List.of` and `Map.of`, in examples. + options.release.set(9) +} + +application { + // Use `./gradlew :stagehand-java-example:run` to run `Main` + // Use `./gradlew :stagehand-java-example:run -Pexample=Something` to run `SomethingExample` + mainClass = "com.stagehand.api.example.${ + if (project.hasProperty("example")) + "${project.property("example")}Example" + else + "Main" + }" +} diff --git a/stagehand-java-lib/.keep b/stagehand-java-lib/.keep new file mode 100644 index 0000000..5e2c99f --- /dev/null +++ b/stagehand-java-lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/stagehand-java-proguard-test/build.gradle.kts b/stagehand-java-proguard-test/build.gradle.kts new file mode 100644 index 0000000..0102d44 --- /dev/null +++ b/stagehand-java-proguard-test/build.gradle.kts @@ -0,0 +1,101 @@ +plugins { + id("stagehand.kotlin") + id("com.gradleup.shadow") version "8.3.8" +} + +buildscript { + repositories { + google() + } + + dependencies { + classpath("com.guardsquare:proguard-gradle:7.4.2") + classpath("com.android.tools:r8:8.3.37") + } +} + +dependencies { + testImplementation(project(":stagehand-java")) + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testImplementation("org.assertj:assertj-core:3.27.7") + testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") +} + +tasks.shadowJar { + from(sourceSets.test.get().output) + configurations = listOf(project.configurations.testRuntimeClasspath.get()) +} + +val proguardJarPath = "${layout.buildDirectory.get()}/libs/${project.name}-${project.version}-proguard.jar" +val proguardJar by tasks.registering(proguard.gradle.ProGuardTask::class) { + group = "verification" + dependsOn(tasks.shadowJar) + notCompatibleWithConfigurationCache("ProGuard") + + injars(tasks.shadowJar) + outjars(proguardJarPath) + printmapping("${layout.buildDirectory.get()}/proguard-mapping.txt") + + val javaHome = System.getProperty("java.home") + if (System.getProperty("java.version").startsWith("1.")) { + // Before Java 9, the runtime classes were packaged in a single jar file. + libraryjars("$javaHome/lib/rt.jar") + } else { + // As of Java 9, the runtime classes are packaged in modular jmod files. + libraryjars( + // Filters must be specified first, as a map. + mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"), + "$javaHome/jmods/java.base.jmod" + ) + } + + configuration("./test.pro") + configuration("../stagehand-java-core/src/main/resources/META-INF/proguard/stagehand-java-core.pro") +} + +val testProGuard by tasks.registering(JavaExec::class) { + group = "verification" + dependsOn(proguardJar) + notCompatibleWithConfigurationCache("ProGuard") + + mainClass.set("com.browserbase.api.proguard.ProGuardCompatibilityTest") + classpath = files(proguardJarPath) +} + +val r8JarPath = "${layout.buildDirectory.get()}/libs/${project.name}-${project.version}-r8.jar" +val r8Jar by tasks.registering(JavaExec::class) { + group = "verification" + dependsOn(tasks.shadowJar) + notCompatibleWithConfigurationCache("R8") + + mainClass.set("com.android.tools.r8.R8") + classpath = buildscript.configurations["classpath"] + + args = listOf( + "--release", + "--classfile", + "--output", r8JarPath, + "--lib", System.getProperty("java.home"), + "--pg-conf", "./test.pro", + "--pg-conf", "../stagehand-java-core/src/main/resources/META-INF/proguard/stagehand-java-core.pro", + "--pg-map-output", "${layout.buildDirectory.get()}/r8-mapping.txt", + tasks.shadowJar.get().archiveFile.get().asFile.absolutePath, + ) +} + +val testR8 by tasks.registering(JavaExec::class) { + group = "verification" + dependsOn(r8Jar) + notCompatibleWithConfigurationCache("R8") + + mainClass.set("com.browserbase.api.proguard.ProGuardCompatibilityTest") + classpath = files(r8JarPath) +} + +tasks.test { + dependsOn(testProGuard) + dependsOn(testR8) + // We defer to the tests run via the ProGuard JAR. + enabled = false +} diff --git a/stagehand-java-proguard-test/src/test/kotlin/com/browserbase/api/proguard/ProGuardCompatibilityTest.kt b/stagehand-java-proguard-test/src/test/kotlin/com/browserbase/api/proguard/ProGuardCompatibilityTest.kt new file mode 100644 index 0000000..1099052 --- /dev/null +++ b/stagehand-java-proguard-test/src/test/kotlin/com/browserbase/api/proguard/ProGuardCompatibilityTest.kt @@ -0,0 +1,75 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.proguard + +import com.browserbase.api.client.okhttp.StagehandOkHttpClient +import com.browserbase.api.core.jsonMapper +import com.browserbase.api.models.sessions.Action +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import kotlin.reflect.full.memberFunctions +import kotlin.reflect.jvm.javaMethod +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class ProGuardCompatibilityTest { + + companion object { + + @JvmStatic + fun main(args: Array) { + // To debug that we're using the right JAR. + val jarPath = this::class.java.getProtectionDomain().codeSource.location + println("JAR being used: $jarPath") + + // We have to manually run the test methods instead of using the JUnit runner because it + // seems impossible to get working with R8. + val test = ProGuardCompatibilityTest() + test::class + .memberFunctions + .asSequence() + .filter { function -> + function.javaMethod?.isAnnotationPresent(Test::class.java) == true + } + .forEach { it.call(test) } + } + } + + @Test + fun proguardRules() { + val rulesFile = + javaClass.classLoader.getResourceAsStream("META-INF/proguard/stagehand-java-core.pro") + + assertThat(rulesFile).isNotNull() + } + + @Test + fun client() { + val client = + StagehandOkHttpClient.builder() + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + + assertThat(client).isNotNull() + assertThat(client.sessions()).isNotNull() + } + + @Test + fun actionRoundtrip() { + val jsonMapper = jsonMapper() + val action = + Action.builder() + .description("Click the submit button") + .selector("[data-testid='submit-button']") + .addArgument("Hello World") + .backendNodeId(0.0) + .method("click") + .build() + + val roundtrippedAction = + jsonMapper.readValue(jsonMapper.writeValueAsString(action), jacksonTypeRef()) + + assertThat(roundtrippedAction).isEqualTo(action) + } +} diff --git a/stagehand-java-proguard-test/test.pro b/stagehand-java-proguard-test/test.pro new file mode 100644 index 0000000..47be604 --- /dev/null +++ b/stagehand-java-proguard-test/test.pro @@ -0,0 +1,9 @@ +# Specify the entrypoint where ProGuard starts to determine what's reachable. +-keep class com.browserbase.api.proguard.** { *; } + +# For the testing framework. +-keep class org.junit.** { *; } + +# Many warnings don't apply for our testing purposes. +-dontnote +-dontwarn \ No newline at end of file diff --git a/stagehand-java/build.gradle.kts b/stagehand-java/build.gradle.kts new file mode 100644 index 0000000..6d32486 --- /dev/null +++ b/stagehand-java/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("stagehand.kotlin") + id("stagehand.publish") +} + +dependencies { + api(project(":stagehand-java-client-okhttp")) +} + +// Redefine `dokkaJavadoc` to: +// - Depend on the root project's task for merging the docs of all the projects +// - Forward that task's output to this task's output +tasks.named("dokkaJavadoc").configure { + actions.clear() + + val dokkaJavadocCollector = rootProject.tasks["dokkaJavadocCollector"] + dependsOn(dokkaJavadocCollector) + + val outputDirectory = project.layout.buildDirectory.dir("dokka/javadoc") + doLast { + copy { + from(dokkaJavadocCollector.outputs.files) + into(outputDirectory) + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + } + + outputs.dir(outputDirectory) +}