diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/generators/OAuthTokenSupplierGenerator.java b/generators/java/sdk/src/main/java/com/fern/java/client/generators/OAuthTokenSupplierGenerator.java index 92bdaee25ae0..0cbea83f46f8 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/generators/OAuthTokenSupplierGenerator.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/generators/OAuthTokenSupplierGenerator.java @@ -18,6 +18,9 @@ import com.fern.ir.model.http.SdkRequestWrapper; import com.fern.ir.model.ir.Subpackage; import com.fern.ir.model.types.ContainerType; +import com.fern.ir.model.types.ObjectProperty; +import com.fern.ir.model.types.ObjectTypeDeclaration; +import com.fern.ir.model.types.TypeDeclaration; import com.fern.ir.model.types.TypeReference; import com.fern.java.client.ClientGeneratorContext; import com.fern.java.client.generators.visitors.RequestPropertyToNameVisitor; @@ -357,6 +360,40 @@ public Void visitInlinedRequestBody(InlinedRequestBody inlinedRequestBody) { @Override public Void visitReference(com.fern.ir.model.http.HttpRequestBodyReference reference) { + TypeReference requestBodyType = reference.getRequestBodyType(); + if (requestBodyType.isNamed()) { + TypeDeclaration typeDeclaration = generatorContext + .getTypeDeclarations() + .get(requestBodyType.getNamed().get().getTypeId()); + if (typeDeclaration != null + && typeDeclaration.getShape().isObject()) { + ObjectTypeDeclaration objectType = + typeDeclaration.getShape().getObject().get(); + for (ObjectProperty prop : objectType.getProperties()) { + String propName = + prop.getName().getName().getCamelCase().getUnsafeName(); + RequestPropertyInfo oauthProp = allOAuthProperties.get(propName); + if (oauthProp == null) { + continue; + } + + processedProperties.add(propName); + boolean isLiteral = isLiteralType(prop.getValueType()); + boolean isOptional = isOptionalType(prop.getValueType()); + + BuilderProperty builderProp = + new BuilderProperty(propName, oauthProp.fieldName, isLiteral); + + if (isLiteral) { + continue; + } else if (isOptional) { + optionalProperties.add(builderProp); + } else { + requiredProperties.add(builderProp); + } + } + } + } return null; } diff --git a/generators/java/sdk/versions.yml b/generators/java/sdk/versions.yml index ad72f088bdf2..5a9867e004b7 100644 --- a/generators/java/sdk/versions.yml +++ b/generators/java/sdk/versions.yml @@ -1,3 +1,14 @@ +- version: 3.23.2 + changelogEntry: + - summary: | + Fix OAuthTokenSupplier builder method ordering to match staged builder sequence. When + generating OAuth token supplier code for request types with staged builders, the builder + method calls are now generated in the correct order based on the type's property declaration + order, preventing compilation errors. + type: fix + createdAt: "2025-12-08" + irVersion: 61 + - version: 3.23.1 changelogEntry: - summary: | diff --git a/seed/java-sdk/java-oauth-staged-builder/default/.fern/metadata.json b/seed/java-sdk/java-oauth-staged-builder/default/.fern/metadata.json new file mode 100644 index 000000000000..79e2bd708539 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/.fern/metadata.json @@ -0,0 +1,5 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-java-sdk", + "generatorVersion": "latest" +} \ No newline at end of file diff --git a/seed/java-sdk/java-oauth-staged-builder/default/.github/workflows/ci.yml b/seed/java-sdk/java-oauth-staged-builder/default/.github/workflows/ci.yml new file mode 100644 index 000000000000..7bffd9de06d8 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Compile + run: ./gradlew compileJava + + test: + needs: [ compile ] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Test + run: ./gradlew test + publish: + needs: [ compile, test ] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Publish to maven + run: | + ./gradlew publish + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLISH_REGISTRY_URL: "" diff --git a/seed/java-sdk/java-oauth-staged-builder/default/.gitignore b/seed/java-sdk/java-oauth-staged-builder/default/.gitignore new file mode 100644 index 000000000000..d4199abc2cd4 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/.gitignore @@ -0,0 +1,24 @@ +*.class +.project +.gradle +? +.classpath +.checkstyle +.settings +.node +build + +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +out/ + +# Eclipse/IntelliJ APT +generated_src/ +generated_testSrc/ +generated/ + +bin +build \ No newline at end of file diff --git a/seed/java-sdk/java-oauth-staged-builder/default/README.md b/seed/java-sdk/java-oauth-staged-builder/default/README.md new file mode 100644 index 000000000000..86338eea8b05 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/README.md @@ -0,0 +1,224 @@ +# Seed Java Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FJava) +[![Maven Central](https://img.shields.io/maven-central/v/com.fern/java-oauth-staged-builder)](https://central.sonatype.com/artifact/com.fern/java-oauth-staged-builder) + +The Seed Java library provides convenient access to the Seed APIs from Java. + +## Table of Contents + +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Base Url](#base-url) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Custom Client](#custom-client) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Custom Headers](#custom-headers) + - [Access Raw Response Data](#access-raw-response-data) +- [Contributing](#contributing) + +## Installation + +### Gradle + +Add the dependency in your `build.gradle` file: + +```groovy +dependencies { + implementation 'com.fern:java-oauth-staged-builder' +} +``` + +### Maven + +Add the dependency in your `pom.xml` file: + +```xml + + com.fern + java-oauth-staged-builder + 0.0.1 + +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```java +package com.example.usage; + +import com.seed.javaOauthStagedBuilder.SeedJavaOauthStagedBuilderClient; +import com.seed.javaOauthStagedBuilder.resources.auth.requests.GetTokenRequest; + +public class Example { + public static void main(String[] args) { + SeedJavaOauthStagedBuilderClient client = SeedJavaOauthStagedBuilderClient + .builder() + .clientId("") + .clientSecret("") + .build(); + + client.auth().getToken( + GetTokenRequest + .builder() + .apiKey("apiKey") + .clientId("clientId") + .clientSecret("clientSecret") + .scope("scope") + .build() + ); + } +} +``` + +## Base Url + +You can set a custom base URL when constructing the client. + +```java +import com.seed.javaOauthStagedBuilder.SeedJavaOauthStagedBuilderClient; + +SeedJavaOauthStagedBuilderClient client = SeedJavaOauthStagedBuilderClient + .builder() + .url("https://example.com") + .build(); +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), an API exception will be thrown. + +```java +import com.seed.javaOauthStagedBuilder.core.SeedJavaOauthStagedBuilderApiException; + +try{ + client.auth().getToken(...); +} catch (SeedJavaOauthStagedBuilderApiException e){ + // Do something with the API exception... +} +``` + +## Advanced + +### Custom Client + +This SDK is built to work with any instance of `OkHttpClient`. By default, if no client is provided, the SDK will construct one. +However, you can pass your own client like so: + +```java +import com.seed.javaOauthStagedBuilder.SeedJavaOauthStagedBuilderClient; +import okhttp3.OkHttpClient; + +OkHttpClient customClient = ...; + +SeedJavaOauthStagedBuilderClient client = SeedJavaOauthStagedBuilderClient + .builder() + .httpClient(customClient) + .build(); +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). Before defaulting to exponential backoff, the SDK will first attempt to respect +the `Retry-After` header (as either in seconds or as an HTTP date), and then the `X-RateLimit-Reset` header +(as a Unix timestamp in epoch seconds); failing both of those, it will fall back to exponential backoff. + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `maxRetries` client option to configure this behavior. + +```java +import com.seed.javaOauthStagedBuilder.SeedJavaOauthStagedBuilderClient; + +SeedJavaOauthStagedBuilderClient client = SeedJavaOauthStagedBuilderClient + .builder() + .maxRetries(1) + .build(); +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. + +```java +import com.seed.javaOauthStagedBuilder.SeedJavaOauthStagedBuilderClient; +import com.seed.javaOauthStagedBuilder.core.RequestOptions; + +// Client level +SeedJavaOauthStagedBuilderClient client = SeedJavaOauthStagedBuilderClient + .builder() + .timeout(10) + .build(); + +// Request level +client.auth().getToken( + ..., + RequestOptions + .builder() + .timeout(10) + .build() +); +``` + +### Custom Headers + +The SDK allows you to add custom headers to requests. You can configure headers at the client level or at the request level. + +```java +import com.seed.javaOauthStagedBuilder.SeedJavaOauthStagedBuilderClient; +import com.seed.javaOauthStagedBuilder.core.RequestOptions; + +// Client level +SeedJavaOauthStagedBuilderClient client = SeedJavaOauthStagedBuilderClient + .builder() + .addHeader("X-Custom-Header", "custom-value") + .addHeader("X-Request-Id", "abc-123") + .build(); +; + +// Request level +client.auth().getToken( + ..., + RequestOptions + .builder() + .addHeader("X-Request-Header", "request-value") + .build() +); +``` + +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `withRawResponse()` method. +The `withRawResponse()` method returns a raw client that wraps all responses with `body()` and `headers()` methods. +(A normal client's `response` is identical to a raw client's `response.body()`.) + +```java +GetTokenHttpResponse response = client.auth().withRawResponse().getToken(...); + +System.out.println(response.body()); +System.out.println(response.headers().get("X-My-Header")); +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! \ No newline at end of file diff --git a/seed/java-sdk/java-oauth-staged-builder/default/build.gradle b/seed/java-sdk/java-oauth-staged-builder/default/build.gradle new file mode 100644 index 000000000000..7a1965d85af5 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/build.gradle @@ -0,0 +1,102 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'com.diffplug.spotless' version '6.11.0' +} + +repositories { + mavenCentral() + maven { + url 'https://s01.oss.sonatype.org/content/repositories/releases/' + } +} + +dependencies { + api 'com.squareup.okhttp3:okhttp:5.2.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.18.2' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' +} + + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +tasks.withType(Javadoc) { + failOnError false + options.addStringOption('Xdoclint:none', '-quiet') +} + +spotless { + java { + palantirJavaFormat() + } +} + + +java { + withSourcesJar() + withJavadocJar() +} + + +group = 'com.fern' + +version = '0.0.1' + +jar { + dependsOn(":generatePomFileForMavenPublication") + archiveBaseName = "java-oauth-staged-builder" +} + +sourcesJar { + archiveBaseName = "java-oauth-staged-builder" +} + +javadocJar { + archiveBaseName = "java-oauth-staged-builder" +} + +test { + useJUnitPlatform() + testLogging { + showStandardStreams = true + } +} + +publishing { + publications { + maven(MavenPublication) { + groupId = 'com.fern' + artifactId = 'java-oauth-staged-builder' + version = '0.0.1' + from components.java + pom { + licenses { + license { + name = 'The MIT License (MIT)' + url = 'https://mit-license.org/' + } + } + scm { + connection = 'scm:git:git://github.com/java-oauth-staged-builder/fern.git' + developerConnection = 'scm:git:git://github.com/java-oauth-staged-builder/fern.git' + url = 'https://github.com/java-oauth-staged-builder/fern' + } + } + } + } + repositories { + maven { + url "$System.env.MAVEN_PUBLISH_REGISTRY_URL" + credentials { + username "$System.env.MAVEN_USERNAME" + password "$System.env.MAVEN_PASSWORD" + } + } + } +} + diff --git a/seed/java-sdk/java-oauth-staged-builder/default/reference.md b/seed/java-sdk/java-oauth-staged-builder/default/reference.md new file mode 100644 index 000000000000..b1e73f10e41f --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/reference.md @@ -0,0 +1,81 @@ +# Reference +## Auth +
client.auth.getToken(request) -> TokenResponse +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.auth().getToken( + GetTokenRequest + .builder() + .apiKey("apiKey") + .clientId("clientId") + .clientSecret("clientSecret") + .scope("scope") + .build() +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**apiKey:** `String` + +
+
+ +
+
+ +**clientId:** `String` + +
+
+ +
+
+ +**clientSecret:** `String` + +
+
+ +
+
+ +**grantType:** `String` + +
+
+ +
+
+ +**scope:** `Optional` + +
+
+
+
+ + +
+
+
diff --git a/seed/java-sdk/java-oauth-staged-builder/default/sample-app/build.gradle b/seed/java-sdk/java-oauth-staged-builder/default/sample-app/build.gradle new file mode 100644 index 000000000000..4ee8f227b7af --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/sample-app/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java-library' +} + +repositories { + mavenCentral() + maven { + url 'https://s01.oss.sonatype.org/content/repositories/releases/' + } +} + +dependencies { + implementation rootProject +} + + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + diff --git a/seed/java-sdk/java-oauth-staged-builder/default/sample-app/src/main/java/sample/App.java b/seed/java-sdk/java-oauth-staged-builder/default/sample-app/src/main/java/sample/App.java new file mode 100644 index 000000000000..7f24634553b7 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/sample-app/src/main/java/sample/App.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package sample; + +import java.lang.String; + +public final class App { + public static void main(String[] args) { + // import com.seed.javaOauthStagedBuilder.AsyncSeedJavaOauthStagedBuilderClient + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/settings.gradle b/seed/java-sdk/java-oauth-staged-builder/default/settings.gradle new file mode 100644 index 000000000000..139a9212b296 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'java-oauth-staged-builder' + +include 'sample-app' \ No newline at end of file diff --git a/seed/java-sdk/java-oauth-staged-builder/default/snippet.json b/seed/java-sdk/java-oauth-staged-builder/default/snippet.json new file mode 100644 index 000000000000..a8d08bd94fdd --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/snippet.json @@ -0,0 +1,18 @@ +{ + "endpoints": [ + { + "example_identifier": "ff228f66", + "id": { + "method": "POST", + "path": "/token", + "identifier_override": "endpoint_auth.getToken" + }, + "snippet": { + "type": "java", + "sync_client": "package com.example.usage;\n\nimport com.seed.javaOauthStagedBuilder.SeedJavaOauthStagedBuilderClient;\nimport com.seed.javaOauthStagedBuilder.resources.auth.requests.GetTokenRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedJavaOauthStagedBuilderClient client = SeedJavaOauthStagedBuilderClient\n .builder()\n .clientId(\"\")\n .clientSecret(\"\")\n .build();\n\n client.auth().getToken(\n GetTokenRequest\n .builder()\n .apiKey(\"apiKey\")\n .clientId(\"clientId\")\n .clientSecret(\"clientSecret\")\n .scope(\"scope\")\n .build()\n );\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.javaOauthStagedBuilder.SeedJavaOauthStagedBuilderClient;\nimport com.seed.javaOauthStagedBuilder.resources.auth.requests.GetTokenRequest;\n\npublic class Example {\n public static void main(String[] args) {\n SeedJavaOauthStagedBuilderClient client = SeedJavaOauthStagedBuilderClient\n .builder()\n .clientId(\"\")\n .clientSecret(\"\")\n .build();\n\n client.auth().getToken(\n GetTokenRequest\n .builder()\n .apiKey(\"apiKey\")\n .clientId(\"clientId\")\n .clientSecret(\"clientSecret\")\n .scope(\"scope\")\n .build()\n );\n }\n}\n" + } + } + ], + "types": {} +} \ No newline at end of file diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/AsyncSeedJavaOauthStagedBuilderClient.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/AsyncSeedJavaOauthStagedBuilderClient.java new file mode 100644 index 000000000000..b32a70586bab --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/AsyncSeedJavaOauthStagedBuilderClient.java @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder; + +import com.seed.javaOauthStagedBuilder.core.ClientOptions; +import com.seed.javaOauthStagedBuilder.core.Suppliers; +import com.seed.javaOauthStagedBuilder.resources.auth.AsyncAuthClient; +import java.util.function.Supplier; + +public class AsyncSeedJavaOauthStagedBuilderClient { + protected final ClientOptions clientOptions; + + protected final Supplier authClient; + + public AsyncSeedJavaOauthStagedBuilderClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.authClient = Suppliers.memoize(() -> new AsyncAuthClient(clientOptions)); + } + + public AsyncAuthClient auth() { + return this.authClient.get(); + } + + public static AsyncSeedJavaOauthStagedBuilderClientBuilder builder() { + return new AsyncSeedJavaOauthStagedBuilderClientBuilder(); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/AsyncSeedJavaOauthStagedBuilderClientBuilder.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/AsyncSeedJavaOauthStagedBuilderClientBuilder.java new file mode 100644 index 000000000000..ec24e1331270 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/AsyncSeedJavaOauthStagedBuilderClientBuilder.java @@ -0,0 +1,234 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder; + +import com.seed.javaOauthStagedBuilder.core.ClientOptions; +import com.seed.javaOauthStagedBuilder.core.Environment; +import com.seed.javaOauthStagedBuilder.core.OAuthTokenSupplier; +import com.seed.javaOauthStagedBuilder.resources.auth.AuthClient; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import okhttp3.OkHttpClient; + +public class AsyncSeedJavaOauthStagedBuilderClientBuilder { + private Optional timeout = Optional.empty(); + + private Optional maxRetries = Optional.empty(); + + private final Map customHeaders = new HashMap<>(); + + private String clientId = null; + + private String clientSecret = null; + + private String apiKey = null; + + private String scope = null; + + private Environment environment; + + private OkHttpClient httpClient; + + /** + * Sets clientId + */ + public AsyncSeedJavaOauthStagedBuilderClientBuilder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + /** + * Sets clientSecret + */ + public AsyncSeedJavaOauthStagedBuilderClientBuilder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + /** + * Sets apiKey + */ + public AsyncSeedJavaOauthStagedBuilderClientBuilder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + /** + * Sets scope + */ + public AsyncSeedJavaOauthStagedBuilderClientBuilder scope(String scope) { + this.scope = scope; + return this; + } + + public AsyncSeedJavaOauthStagedBuilderClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public AsyncSeedJavaOauthStagedBuilderClientBuilder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public AsyncSeedJavaOauthStagedBuilderClientBuilder maxRetries(int maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public AsyncSeedJavaOauthStagedBuilderClientBuilder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Add a custom header to be sent with all requests. + * For headers that need to be computed dynamically or conditionally, use the setAdditional() method override instead. + * + * @param name The header name + * @param value The header value + * @return This builder for method chaining + */ + public AsyncSeedJavaOauthStagedBuilderClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); + return this; + } + + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(builder); + setAuthentication(builder); + setHttpClient(builder); + setTimeouts(builder); + setRetries(builder); + for (Map.Entry header : this.customHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + setAdditional(builder); + return builder.build(); + } + + /** + * Sets the environment configuration for the client. + * Override this method to modify URLs or add environment-specific logic. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setEnvironment(ClientOptions.Builder builder) { + builder.environment(this.environment); + } + + /** + * Override this method to customize authentication. + * This method is called during client options construction to set up authentication headers. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAuthentication(ClientOptions.Builder builder) {
+     *     super.setAuthentication(builder); // Keep existing auth
+     *     builder.addHeader("X-API-Key", this.apiKey);
+     * }
+     * }
+ */ + protected void setAuthentication(ClientOptions.Builder builder) { + if (this.clientId != null && this.clientSecret != null) { + ClientOptions.Builder authClientOptionsBuilder = + ClientOptions.builder().environment(this.environment); + AuthClient authClient = new AuthClient(authClientOptionsBuilder.build()); + OAuthTokenSupplier oAuthTokenSupplier = + new OAuthTokenSupplier(this.clientId, this.clientSecret, this.apiKey, this.scope, authClient); + builder.addHeader("Authorization", oAuthTokenSupplier); + } + } + + /** + * Sets the request timeout configuration. + * Override this method to customize timeout behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setTimeouts(ClientOptions.Builder builder) { + if (this.timeout.isPresent()) { + builder.timeout(this.timeout.get()); + } + } + + /** + * Sets the retry configuration for failed requests. + * Override this method to implement custom retry strategies. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setRetries(ClientOptions.Builder builder) { + if (this.maxRetries.isPresent()) { + builder.maxRetries(this.maxRetries.get()); + } + } + + /** + * Sets the OkHttp client configuration. + * Override this method to customize HTTP client behavior (interceptors, connection pools, etc). + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setHttpClient(ClientOptions.Builder builder) { + if (this.httpClient != null) { + builder.httpClient(this.httpClient); + } + } + + /** + * Override this method to add any additional configuration to the client. + * This method is called at the end of the configuration chain, allowing you to add + * custom headers, modify settings, or perform any other client customization. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAdditional(ClientOptions.Builder builder) {
+     *     builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
+     *     builder.addHeader("X-Client-Version", "1.0.0");
+     * }
+     * }
+ */ + protected void setAdditional(ClientOptions.Builder builder) {} + + /** + * Override this method to add custom validation logic before the client is built. + * This method is called at the beginning of the build() method to ensure the configuration is valid. + * Throw an exception to prevent client creation if validation fails. + * + * Example: + *
{@code
+     * @Override
+     * protected void validateConfiguration() {
+     *     super.validateConfiguration(); // Run parent validations
+     *     if (tenantId == null || tenantId.isEmpty()) {
+     *         throw new IllegalStateException("tenantId is required");
+     *     }
+     * }
+     * }
+ */ + protected void validateConfiguration() {} + + public AsyncSeedJavaOauthStagedBuilderClient build() { + validateConfiguration(); + return new AsyncSeedJavaOauthStagedBuilderClient(buildClientOptions()); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/SeedJavaOauthStagedBuilderClient.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/SeedJavaOauthStagedBuilderClient.java new file mode 100644 index 000000000000..6d9db8317f5c --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/SeedJavaOauthStagedBuilderClient.java @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder; + +import com.seed.javaOauthStagedBuilder.core.ClientOptions; +import com.seed.javaOauthStagedBuilder.core.Suppliers; +import com.seed.javaOauthStagedBuilder.resources.auth.AuthClient; +import java.util.function.Supplier; + +public class SeedJavaOauthStagedBuilderClient { + protected final ClientOptions clientOptions; + + protected final Supplier authClient; + + public SeedJavaOauthStagedBuilderClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.authClient = Suppliers.memoize(() -> new AuthClient(clientOptions)); + } + + public AuthClient auth() { + return this.authClient.get(); + } + + public static SeedJavaOauthStagedBuilderClientBuilder builder() { + return new SeedJavaOauthStagedBuilderClientBuilder(); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/SeedJavaOauthStagedBuilderClientBuilder.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/SeedJavaOauthStagedBuilderClientBuilder.java new file mode 100644 index 000000000000..8ae9639677a5 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/SeedJavaOauthStagedBuilderClientBuilder.java @@ -0,0 +1,234 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder; + +import com.seed.javaOauthStagedBuilder.core.ClientOptions; +import com.seed.javaOauthStagedBuilder.core.Environment; +import com.seed.javaOauthStagedBuilder.core.OAuthTokenSupplier; +import com.seed.javaOauthStagedBuilder.resources.auth.AuthClient; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import okhttp3.OkHttpClient; + +public class SeedJavaOauthStagedBuilderClientBuilder { + private Optional timeout = Optional.empty(); + + private Optional maxRetries = Optional.empty(); + + private final Map customHeaders = new HashMap<>(); + + private String clientId = null; + + private String clientSecret = null; + + private String apiKey = null; + + private String scope = null; + + private Environment environment; + + private OkHttpClient httpClient; + + /** + * Sets clientId + */ + public SeedJavaOauthStagedBuilderClientBuilder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + /** + * Sets clientSecret + */ + public SeedJavaOauthStagedBuilderClientBuilder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + /** + * Sets apiKey + */ + public SeedJavaOauthStagedBuilderClientBuilder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + /** + * Sets scope + */ + public SeedJavaOauthStagedBuilderClientBuilder scope(String scope) { + this.scope = scope; + return this; + } + + public SeedJavaOauthStagedBuilderClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public SeedJavaOauthStagedBuilderClientBuilder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public SeedJavaOauthStagedBuilderClientBuilder maxRetries(int maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public SeedJavaOauthStagedBuilderClientBuilder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Add a custom header to be sent with all requests. + * For headers that need to be computed dynamically or conditionally, use the setAdditional() method override instead. + * + * @param name The header name + * @param value The header value + * @return This builder for method chaining + */ + public SeedJavaOauthStagedBuilderClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); + return this; + } + + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(builder); + setAuthentication(builder); + setHttpClient(builder); + setTimeouts(builder); + setRetries(builder); + for (Map.Entry header : this.customHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + setAdditional(builder); + return builder.build(); + } + + /** + * Sets the environment configuration for the client. + * Override this method to modify URLs or add environment-specific logic. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setEnvironment(ClientOptions.Builder builder) { + builder.environment(this.environment); + } + + /** + * Override this method to customize authentication. + * This method is called during client options construction to set up authentication headers. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAuthentication(ClientOptions.Builder builder) {
+     *     super.setAuthentication(builder); // Keep existing auth
+     *     builder.addHeader("X-API-Key", this.apiKey);
+     * }
+     * }
+ */ + protected void setAuthentication(ClientOptions.Builder builder) { + if (this.clientId != null && this.clientSecret != null) { + ClientOptions.Builder authClientOptionsBuilder = + ClientOptions.builder().environment(this.environment); + AuthClient authClient = new AuthClient(authClientOptionsBuilder.build()); + OAuthTokenSupplier oAuthTokenSupplier = + new OAuthTokenSupplier(this.clientId, this.clientSecret, this.apiKey, this.scope, authClient); + builder.addHeader("Authorization", oAuthTokenSupplier); + } + } + + /** + * Sets the request timeout configuration. + * Override this method to customize timeout behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setTimeouts(ClientOptions.Builder builder) { + if (this.timeout.isPresent()) { + builder.timeout(this.timeout.get()); + } + } + + /** + * Sets the retry configuration for failed requests. + * Override this method to implement custom retry strategies. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setRetries(ClientOptions.Builder builder) { + if (this.maxRetries.isPresent()) { + builder.maxRetries(this.maxRetries.get()); + } + } + + /** + * Sets the OkHttp client configuration. + * Override this method to customize HTTP client behavior (interceptors, connection pools, etc). + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setHttpClient(ClientOptions.Builder builder) { + if (this.httpClient != null) { + builder.httpClient(this.httpClient); + } + } + + /** + * Override this method to add any additional configuration to the client. + * This method is called at the end of the configuration chain, allowing you to add + * custom headers, modify settings, or perform any other client customization. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAdditional(ClientOptions.Builder builder) {
+     *     builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
+     *     builder.addHeader("X-Client-Version", "1.0.0");
+     * }
+     * }
+ */ + protected void setAdditional(ClientOptions.Builder builder) {} + + /** + * Override this method to add custom validation logic before the client is built. + * This method is called at the beginning of the build() method to ensure the configuration is valid. + * Throw an exception to prevent client creation if validation fails. + * + * Example: + *
{@code
+     * @Override
+     * protected void validateConfiguration() {
+     *     super.validateConfiguration(); // Run parent validations
+     *     if (tenantId == null || tenantId.isEmpty()) {
+     *         throw new IllegalStateException("tenantId is required");
+     *     }
+     * }
+     * }
+ */ + protected void validateConfiguration() {} + + public SeedJavaOauthStagedBuilderClient build() { + validateConfiguration(); + return new SeedJavaOauthStagedBuilderClient(buildClientOptions()); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ClientOptions.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ClientOptions.java new file mode 100644 index 000000000000..1a321b52f904 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ClientOptions.java @@ -0,0 +1,189 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import okhttp3.OkHttpClient; + +public final class ClientOptions { + private final Environment environment; + + private final Map headers; + + private final Map> headerSuppliers; + + private final OkHttpClient httpClient; + + private final int timeout; + + private final int maxRetries; + + private ClientOptions( + Environment environment, + Map headers, + Map> headerSuppliers, + OkHttpClient httpClient, + int timeout, + int maxRetries) { + this.environment = environment; + this.headers = new HashMap<>(); + this.headers.putAll(headers); + this.headers.putAll(new HashMap() { + { + put("User-Agent", "com.fern:java-oauth-staged-builder/0.0.1"); + put("X-Fern-Language", "JAVA"); + } + }); + this.headerSuppliers = headerSuppliers; + this.httpClient = httpClient; + this.timeout = timeout; + this.maxRetries = maxRetries; + } + + public Environment environment() { + return this.environment; + } + + public Map headers(RequestOptions requestOptions) { + Map values = new HashMap<>(this.headers); + headerSuppliers.forEach((key, supplier) -> { + values.put(key, supplier.get()); + }); + if (requestOptions != null) { + values.putAll(requestOptions.getHeaders()); + } + return values; + } + + public int timeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.timeout; + } + return requestOptions.getTimeout().orElse(this.timeout); + } + + public OkHttpClient httpClient() { + return this.httpClient; + } + + public OkHttpClient httpClientWithTimeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.httpClient; + } + return this.httpClient + .newBuilder() + .callTimeout(requestOptions.getTimeout().get(), requestOptions.getTimeoutTimeUnit()) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .build(); + } + + public int maxRetries() { + return this.maxRetries; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Environment environment; + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private int maxRetries = 2; + + private Optional timeout = Optional.empty(); + + private OkHttpClient httpClient = null; + + public Builder environment(Environment environment) { + this.environment = environment; + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(Optional timeout) { + this.timeout = timeout; + return this; + } + + /** + * Override the maximum number of retries. Defaults to 2 retries. + */ + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + public Builder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + public ClientOptions build() { + OkHttpClient.Builder httpClientBuilder = + this.httpClient != null ? this.httpClient.newBuilder() : new OkHttpClient.Builder(); + + if (this.httpClient != null) { + timeout.ifPresent(timeout -> httpClientBuilder + .callTimeout(timeout, TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS)); + } else { + httpClientBuilder + .callTimeout(this.timeout.orElse(60), TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .addInterceptor(new RetryInterceptor(this.maxRetries)); + } + + this.httpClient = httpClientBuilder.build(); + this.timeout = Optional.of(httpClient.callTimeoutMillis() / 1000); + + return new ClientOptions( + environment, headers, headerSuppliers, httpClient, this.timeout.get(), this.maxRetries); + } + + /** + * Create a new Builder initialized with values from an existing ClientOptions + */ + public static Builder from(ClientOptions clientOptions) { + Builder builder = new Builder(); + builder.environment = clientOptions.environment(); + builder.timeout = Optional.of(clientOptions.timeout(null)); + builder.httpClient = clientOptions.httpClient(); + return builder; + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/DateTimeDeserializer.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/DateTimeDeserializer.java new file mode 100644 index 000000000000..a4b41c40f80d --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/DateTimeDeserializer.java @@ -0,0 +1,55 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; + +/** + * Custom deserializer that handles converting ISO8601 dates into {@link OffsetDateTime} objects. + */ +class DateTimeDeserializer extends JsonDeserializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule().addDeserializer(OffsetDateTime.class, new DateTimeDeserializer()); + } + + /** + * Gets a module wrapping this deserializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonToken token = parser.currentToken(); + if (token == JsonToken.VALUE_NUMBER_INT) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC); + } else { + TemporalAccessor temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest( + parser.getValueAsString(), OffsetDateTime::from, LocalDateTime::from); + + if (temporal.query(TemporalQueries.offset()) == null) { + return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC); + } else { + return OffsetDateTime.from(temporal); + } + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Environment.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Environment.java new file mode 100644 index 000000000000..dfbb9c534eae --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Environment.java @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +public final class Environment { + private final String url; + + private Environment(String url) { + this.url = url; + } + + public String getUrl() { + return this.url; + } + + public static Environment custom(String url) { + return new Environment(url); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/FileStream.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/FileStream.java new file mode 100644 index 000000000000..e7a0e9dbe772 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/FileStream.java @@ -0,0 +1,60 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a file stream with associated metadata for file uploads. + */ +public class FileStream { + private final InputStream inputStream; + private final String fileName; + private final MediaType contentType; + + /** + * Constructs a FileStream with the given input stream and optional metadata. + * + * @param inputStream The input stream of the file content. Must not be null. + * @param fileName The name of the file, or null if unknown. + * @param contentType The MIME type of the file content, or null if unknown. + * @throws NullPointerException if inputStream is null + */ + public FileStream(InputStream inputStream, @Nullable String fileName, @Nullable MediaType contentType) { + this.inputStream = Objects.requireNonNull(inputStream, "Input stream cannot be null"); + this.fileName = fileName; + this.contentType = contentType; + } + + public FileStream(InputStream inputStream) { + this(inputStream, null, null); + } + + public InputStream getInputStream() { + return inputStream; + } + + @Nullable + public String getFileName() { + return fileName; + } + + @Nullable + public MediaType getContentType() { + return contentType; + } + + /** + * Creates a RequestBody suitable for use with OkHttp client. + * + * @return A RequestBody instance representing this file stream. + */ + public RequestBody toRequestBody() { + return new InputStreamRequestBody(contentType, inputStream); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/InputStreamRequestBody.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/InputStreamRequestBody.java new file mode 100644 index 000000000000..3b65c39dc2fe --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/InputStreamRequestBody.java @@ -0,0 +1,74 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; +import org.jetbrains.annotations.Nullable; + +/** + * A custom implementation of OkHttp's RequestBody that wraps an InputStream. + * This class allows streaming of data from an InputStream directly to an HTTP request body, + * which is useful for file uploads or sending large amounts of data without loading it all into memory. + */ +public class InputStreamRequestBody extends RequestBody { + private final InputStream inputStream; + private final MediaType contentType; + + /** + * Constructs an InputStreamRequestBody with the specified content type and input stream. + * + * @param contentType the MediaType of the content, or null if not known + * @param inputStream the InputStream containing the data to be sent + * @throws NullPointerException if inputStream is null + */ + public InputStreamRequestBody(@Nullable MediaType contentType, InputStream inputStream) { + this.contentType = contentType; + this.inputStream = Objects.requireNonNull(inputStream, "inputStream == null"); + } + + /** + * Returns the content type of this request body. + * + * @return the MediaType of the content, or null if not specified + */ + @Nullable + @Override + public MediaType contentType() { + return contentType; + } + + /** + * Returns the content length of this request body, if known. + * This method attempts to determine the length using the InputStream's available() method, + * which may not always accurately reflect the total length of the stream. + * + * @return the content length, or -1 if the length is unknown + * @throws IOException if an I/O error occurs + */ + @Override + public long contentLength() throws IOException { + return inputStream.available() == 0 ? -1 : inputStream.available(); + } + + /** + * Writes the content of the InputStream to the given BufferedSink. + * This method is responsible for transferring the data from the InputStream to the network request. + * + * @param sink the BufferedSink to write the content to + * @throws IOException if an I/O error occurs during writing + */ + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (Source source = Okio.source(inputStream)) { + sink.writeAll(source); + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/MediaTypes.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/MediaTypes.java new file mode 100644 index 000000000000..fa6c70a644a5 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/MediaTypes.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import okhttp3.MediaType; + +public final class MediaTypes { + + public static final MediaType APPLICATION_JSON = MediaType.parse("application/json"); + + private MediaTypes() {} +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Nullable.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Nullable.java new file mode 100644 index 000000000000..a98db5b3c638 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Nullable.java @@ -0,0 +1,140 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.util.Optional; +import java.util.function.Function; + +public final class Nullable { + + private final Either, Null> value; + + private Nullable() { + this.value = Either.left(Optional.empty()); + } + + private Nullable(T value) { + if (value == null) { + this.value = Either.right(Null.INSTANCE); + } else { + this.value = Either.left(Optional.of(value)); + } + } + + public static Nullable ofNull() { + return new Nullable<>(null); + } + + public static Nullable of(T value) { + return new Nullable<>(value); + } + + public static Nullable empty() { + return new Nullable<>(); + } + + public static Nullable ofOptional(Optional value) { + if (value.isPresent()) { + return of(value.get()); + } else { + return empty(); + } + } + + public boolean isNull() { + return this.value.isRight(); + } + + public boolean isEmpty() { + return this.value.isLeft() && !this.value.getLeft().isPresent(); + } + + public T get() { + if (this.isNull()) { + return null; + } + + return this.value.getLeft().get(); + } + + public Nullable map(Function mapper) { + if (this.isNull()) { + return Nullable.ofNull(); + } + + return Nullable.ofOptional(this.value.getLeft().map(mapper)); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Nullable)) { + return false; + } + + if (((Nullable) other).isNull() && this.isNull()) { + return true; + } + + return this.value.getLeft().equals(((Nullable) other).value.getLeft()); + } + + private static final class Either { + private L left = null; + private R right = null; + + private Either(L left, R right) { + if (left != null && right != null) { + throw new IllegalArgumentException("Left and right argument cannot both be non-null."); + } + + if (left == null && right == null) { + throw new IllegalArgumentException("Left and right argument cannot both be null."); + } + + if (left != null) { + this.left = left; + } + + if (right != null) { + this.right = right; + } + } + + public static Either left(L left) { + return new Either<>(left, null); + } + + public static Either right(R right) { + return new Either<>(null, right); + } + + public boolean isLeft() { + return this.left != null; + } + + public boolean isRight() { + return this.right != null; + } + + public L getLeft() { + if (!this.isLeft()) { + throw new IllegalArgumentException("Cannot get left from right Either."); + } + return this.left; + } + + public R getRight() { + if (!this.isRight()) { + throw new IllegalArgumentException("Cannot get right from left Either."); + } + return this.right; + } + } + + private static final class Null { + private static final Null INSTANCE = new Null(); + + private Null() {} + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/NullableNonemptyFilter.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/NullableNonemptyFilter.java new file mode 100644 index 000000000000..5249d7c23a6b --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/NullableNonemptyFilter.java @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.util.Optional; + +public final class NullableNonemptyFilter { + @Override + public boolean equals(Object o) { + boolean isOptionalEmpty = isOptionalEmpty(o); + + return isOptionalEmpty; + } + + private boolean isOptionalEmpty(Object o) { + if (o instanceof Optional) { + return !((Optional) o).isPresent(); + } + return false; + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/OAuthTokenSupplier.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/OAuthTokenSupplier.java new file mode 100644 index 000000000000..2ea66bb414a9 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/OAuthTokenSupplier.java @@ -0,0 +1,63 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import com.seed.javaOauthStagedBuilder.resources.auth.AuthClient; +import com.seed.javaOauthStagedBuilder.resources.auth.requests.GetTokenRequest; +import com.seed.javaOauthStagedBuilder.resources.auth.types.TokenResponse; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.function.Supplier; + +public final class OAuthTokenSupplier implements Supplier { + private static final long BUFFER_IN_MINUTES = 2; + + private final String apiKey; + + private final String clientId; + + private final String clientSecret; + + private final String scope; + + private final AuthClient authClient; + + private String accessToken; + + private Instant expiresAt; + + public OAuthTokenSupplier( + String apiKey, String clientId, String clientSecret, String scope, AuthClient authClient) { + this.apiKey = apiKey; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.scope = scope; + this.authClient = authClient; + this.expiresAt = Instant.now(); + } + + public TokenResponse fetchToken() { + GetTokenRequest getTokenRequest = GetTokenRequest.builder() + .apiKey(apiKey) + .clientId(clientId) + .clientSecret(clientSecret) + .scope(scope) + .build(); + return authClient.getToken(getTokenRequest); + } + + @java.lang.Override + public String get() { + if (accessToken == null || expiresAt.isBefore(Instant.now())) { + TokenResponse authResponse = fetchToken(); + this.accessToken = authResponse.getAccessToken(); + this.expiresAt = getExpiresAt(authResponse.getExpiresIn()); + } + return "Bearer " + accessToken; + } + + private Instant getExpiresAt(long expiresInSeconds) { + return Instant.now().plus(expiresInSeconds, ChronoUnit.SECONDS).minus(BUFFER_IN_MINUTES, ChronoUnit.MINUTES); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ObjectMappers.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ObjectMappers.java new file mode 100644 index 000000000000..dbe0848e60e6 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ObjectMappers.java @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; + +public final class ObjectMappers { + public static final ObjectMapper JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .addModule(DateTimeDeserializer.getModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + private ObjectMappers() {} + + public static String stringify(Object o) { + try { + return JSON_MAPPER + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(o); + } catch (IOException e) { + return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); + } + } + + public static Object parseErrorBody(String responseBodyString) { + try { + return JSON_MAPPER.readValue(responseBodyString, Object.class); + } catch (JsonProcessingException ignored) { + return responseBodyString; + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/QueryStringMapper.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/QueryStringMapper.java new file mode 100644 index 000000000000..e95eaeb75fed --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/QueryStringMapper.java @@ -0,0 +1,142 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import okhttp3.HttpUrl; +import okhttp3.MultipartBody; + +public class QueryStringMapper { + + private static final ObjectMapper MAPPER = ObjectMappers.JSON_MAPPER; + + public static void addQueryParameter(HttpUrl.Builder httpUrl, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + httpUrl.addQueryParameter(key, valueNode.textValue()); + } else { + httpUrl.addQueryParameter(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().textValue()); + } else { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().toString()); + } + } + } + + public static void addFormDataPart( + MultipartBody.Builder multipartBody, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + multipartBody.addFormDataPart(key, valueNode.textValue()); + } else { + multipartBody.addFormDataPart(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().textValue()); + } else { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().toString()); + } + } + } + + public static List> flattenObject(ObjectNode object, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator> fields = object.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + + String key = "[" + field.getKey() + "]"; + + if (field.getValue().isObject()) { + List> flatField = + flattenObject((ObjectNode) field.getValue(), arraysAsRepeats); + addAll(flat, flatField, key); + } else if (field.getValue().isArray()) { + List> flatField = + flattenArray((ArrayNode) field.getValue(), key, arraysAsRepeats); + addAll(flat, flatField, ""); + } else { + flat.add(new AbstractMap.SimpleEntry<>(key, field.getValue())); + } + } + + return flat; + } + + private static List> flattenArray( + ArrayNode array, String key, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator elements = array.elements(); + + int index = 0; + while (elements.hasNext()) { + JsonNode element = elements.next(); + + String indexKey = key + "[" + index + "]"; + + if (arraysAsRepeats) { + indexKey = key; + } + + if (element.isObject()) { + List> flatField = flattenObject((ObjectNode) element, arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else if (element.isArray()) { + List> flatField = flattenArray((ArrayNode) element, "", arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else { + flat.add(new AbstractMap.SimpleEntry<>(indexKey, element)); + } + + index++; + } + + return flat; + } + + private static void addAll( + List> target, List> source, String prefix) { + for (Map.Entry entry : source) { + Map.Entry entryToAdd = + new AbstractMap.SimpleEntry<>(prefix + entry.getKey(), entry.getValue()); + target.add(entryToAdd); + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/RequestOptions.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/RequestOptions.java new file mode 100644 index 000000000000..3aac317a234e --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/RequestOptions.java @@ -0,0 +1,87 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class RequestOptions { + private final Optional timeout; + + private final TimeUnit timeoutTimeUnit; + + private final Map headers; + + private final Map> headerSuppliers; + + private RequestOptions( + Optional timeout, + TimeUnit timeoutTimeUnit, + Map headers, + Map> headerSuppliers) { + this.timeout = timeout; + this.timeoutTimeUnit = timeoutTimeUnit; + this.headers = headers; + this.headerSuppliers = headerSuppliers; + } + + public Optional getTimeout() { + return timeout; + } + + public TimeUnit getTimeoutTimeUnit() { + return timeoutTimeUnit; + } + + public Map getHeaders() { + Map headers = new HashMap<>(); + headers.putAll(this.headers); + this.headerSuppliers.forEach((key, supplier) -> { + headers.put(key, supplier.get()); + }); + return headers; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Optional timeout = Optional.empty(); + + private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS; + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + public Builder timeout(Integer timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + public Builder timeout(Integer timeout, TimeUnit timeoutTimeUnit) { + this.timeout = Optional.of(timeout); + this.timeoutTimeUnit = timeoutTimeUnit; + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + public RequestOptions build() { + return new RequestOptions(timeout, timeoutTimeUnit, headers, headerSuppliers); + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ResponseBodyInputStream.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ResponseBodyInputStream.java new file mode 100644 index 000000000000..db9822dcb409 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ResponseBodyInputStream.java @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.io.FilterInputStream; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom InputStream that wraps the InputStream from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the stream is closed. + * + * This class extends FilterInputStream and takes an OkHttp Response object as a parameter. + * It retrieves the InputStream from the Response and overrides the close method to close + * both the InputStream and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyInputStream extends FilterInputStream { + private final Response response; + + /** + * Constructs a ResponseBodyInputStream that wraps the InputStream from the given OkHttp + * Response object. + * + * @param response the OkHttp Response object from which the InputStream is retrieved + * @throws IOException if an I/O error occurs while retrieving the InputStream + */ + public ResponseBodyInputStream(Response response) throws IOException { + super(response.body().byteStream()); + this.response = response; + } + + /** + * Closes the InputStream and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the stream is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the stream is closed + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ResponseBodyReader.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ResponseBodyReader.java new file mode 100644 index 000000000000..d7141c151b71 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/ResponseBodyReader.java @@ -0,0 +1,44 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.io.FilterReader; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom Reader that wraps the Reader from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the reader is closed. + * + * This class extends FilterReader and takes an OkHttp Response object as a parameter. + * It retrieves the Reader from the Response and overrides the close method to close + * both the Reader and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyReader extends FilterReader { + private final Response response; + + /** + * Constructs a ResponseBodyReader that wraps the Reader from the given OkHttp Response object. + * + * @param response the OkHttp Response object from which the Reader is retrieved + * @throws IOException if an I/O error occurs while retrieving the Reader + */ + public ResponseBodyReader(Response response) throws IOException { + super(response.body().charStream()); + this.response = response; + } + + /** + * Closes the Reader and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the reader is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the reader is closed + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/RetryInterceptor.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/RetryInterceptor.java new file mode 100644 index 000000000000..2691ffec9c19 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/RetryInterceptor.java @@ -0,0 +1,180 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.io.IOException; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; +import java.util.Random; +import okhttp3.Interceptor; +import okhttp3.Response; + +public class RetryInterceptor implements Interceptor { + + private static final Duration INITIAL_RETRY_DELAY = Duration.ofMillis(1000); + private static final Duration MAX_RETRY_DELAY = Duration.ofMillis(60000); + private static final double JITTER_FACTOR = 0.2; + + private final ExponentialBackoff backoff; + private final Random random = new Random(); + + public RetryInterceptor(int maxRetries) { + this.backoff = new ExponentialBackoff(maxRetries); + } + + @Override + public Response intercept(Chain chain) throws IOException { + Response response = chain.proceed(chain.request()); + + if (shouldRetry(response.code())) { + return retryChain(response, chain); + } + + return response; + } + + private Response retryChain(Response response, Chain chain) throws IOException { + Optional nextBackoff = this.backoff.nextBackoff(response); + while (nextBackoff.isPresent()) { + try { + Thread.sleep(nextBackoff.get().toMillis()); + } catch (InterruptedException e) { + throw new IOException("Interrupted while trying request", e); + } + response.close(); + response = chain.proceed(chain.request()); + if (shouldRetry(response.code())) { + nextBackoff = this.backoff.nextBackoff(response); + } else { + return response; + } + } + + return response; + } + + /** + * Calculates the retry delay from response headers, with fallback to exponential backoff. + * Priority: Retry-After > X-RateLimit-Reset > Exponential Backoff + */ + private Duration getRetryDelayFromHeaders(Response response, int retryAttempt) { + // Check for Retry-After header first (RFC 7231), with no jitter + String retryAfter = response.header("Retry-After"); + if (retryAfter != null) { + // Parse as number of seconds... + Optional secondsDelay = tryParseLong(retryAfter) + .map(seconds -> seconds * 1000) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, MAX_RETRY_DELAY.toMillis())) + .map(Duration::ofMillis); + if (secondsDelay.isPresent()) { + return secondsDelay.get(); + } + + // ...or as an HTTP date; both are valid + Optional dateDelay = tryParseHttpDate(retryAfter) + .map(resetTime -> resetTime.toInstant().toEpochMilli() - System.currentTimeMillis()) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, MAX_RETRY_DELAY.toMillis())) + .map(Duration::ofMillis); + if (dateDelay.isPresent()) { + return dateDelay.get(); + } + } + + // Then check for industry-standard X-RateLimit-Reset header, with positive jitter + String rateLimitReset = response.header("X-RateLimit-Reset"); + if (rateLimitReset != null) { + // Assume Unix timestamp in epoch seconds + Optional rateLimitDelay = tryParseLong(rateLimitReset) + .map(resetTimeSeconds -> (resetTimeSeconds * 1000) - System.currentTimeMillis()) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, MAX_RETRY_DELAY.toMillis())) + .map(this::addPositiveJitter) + .map(Duration::ofMillis); + if (rateLimitDelay.isPresent()) { + return rateLimitDelay.get(); + } + } + + // Fall back to exponential backoff, with symmetric jitter + long baseDelay = INITIAL_RETRY_DELAY.toMillis() * (1L << retryAttempt); // 2^retryAttempt + long cappedDelay = Math.min(baseDelay, MAX_RETRY_DELAY.toMillis()); + return Duration.ofMillis(addSymmetricJitter(cappedDelay)); + } + + /** + * Attempts to parse a string as a long, returning empty Optional on failure. + */ + private Optional tryParseLong(String value) { + if (value == null) { + return Optional.empty(); + } + try { + return Optional.of(Long.parseLong(value)); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + + /** + * Attempts to parse a string as an HTTP date (RFC 1123), returning empty Optional on failure. + */ + private Optional tryParseHttpDate(String value) { + if (value == null) { + return Optional.empty(); + } + try { + return Optional.of(ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME)); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } + + /** + * Adds positive jitter (100-120% of original value) to prevent thundering herd. + * Used for X-RateLimit-Reset header delays. + */ + private long addPositiveJitter(long delayMs) { + double jitterMultiplier = 1.0 + (random.nextDouble() * JITTER_FACTOR); + return (long) (delayMs * jitterMultiplier); + } + + /** + * Adds symmetric jitter (90-110% of original value) to prevent thundering herd. + * Used for exponential backoff delays. + */ + private long addSymmetricJitter(long delayMs) { + double jitterMultiplier = 1.0 + ((random.nextDouble() - 0.5) * JITTER_FACTOR); + return (long) (delayMs * jitterMultiplier); + } + + private static boolean shouldRetry(int statusCode) { + return statusCode == 408 || statusCode == 429 || statusCode >= 500; + } + + private final class ExponentialBackoff { + + private final int maxNumRetries; + + private int retryNumber = 0; + + ExponentialBackoff(int maxNumRetries) { + this.maxNumRetries = maxNumRetries; + } + + public Optional nextBackoff(Response response) { + if (retryNumber >= maxNumRetries) { + return Optional.empty(); + } + + Duration delay = getRetryDelayFromHeaders(response, retryNumber); + retryNumber += 1; + return Optional.of(delay); + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/SeedJavaOauthStagedBuilderApiException.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/SeedJavaOauthStagedBuilderApiException.java new file mode 100644 index 000000000000..b00f74d2944b --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/SeedJavaOauthStagedBuilderApiException.java @@ -0,0 +1,73 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Response; + +/** + * This exception type will be thrown for any non-2XX API responses. + */ +public class SeedJavaOauthStagedBuilderApiException extends SeedJavaOauthStagedBuilderException { + /** + * The error code of the response that triggered the exception. + */ + private final int statusCode; + + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + private final Map> headers; + + public SeedJavaOauthStagedBuilderApiException(String message, int statusCode, Object body) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + } + + public SeedJavaOauthStagedBuilderApiException(String message, int statusCode, Object body, Response rawResponse) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + this.headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + } + + /** + * @return the statusCode + */ + public int statusCode() { + return this.statusCode; + } + + /** + * @return the body + */ + public Object body() { + return this.body; + } + + /** + * @return the headers + */ + public Map> headers() { + return this.headers; + } + + @Override + public String toString() { + return "SeedJavaOauthStagedBuilderApiException{" + "message: " + getMessage() + ", statusCode: " + statusCode + + ", body: " + ObjectMappers.stringify(body) + "}"; + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/SeedJavaOauthStagedBuilderException.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/SeedJavaOauthStagedBuilderException.java new file mode 100644 index 000000000000..53597da29edb --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/SeedJavaOauthStagedBuilderException.java @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +/** + * This class serves as the base exception for all errors in the SDK. + */ +public class SeedJavaOauthStagedBuilderException extends RuntimeException { + public SeedJavaOauthStagedBuilderException(String message) { + super(message); + } + + public SeedJavaOauthStagedBuilderException(String message, Exception e) { + super(message, e); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/SeedJavaOauthStagedBuilderHttpResponse.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/SeedJavaOauthStagedBuilderHttpResponse.java new file mode 100644 index 000000000000..f584834bc15a --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/SeedJavaOauthStagedBuilderHttpResponse.java @@ -0,0 +1,37 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Response; + +public final class SeedJavaOauthStagedBuilderHttpResponse { + + private final T body; + + private final Map> headers; + + public SeedJavaOauthStagedBuilderHttpResponse(T body, Response rawResponse) { + this.body = body; + + Map> headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + this.headers = headers; + } + + public T body() { + return this.body; + } + + public Map> headers() { + return headers; + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Stream.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Stream.java new file mode 100644 index 000000000000..51e30873f2b7 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Stream.java @@ -0,0 +1,302 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Scanner; + +/** + * The {@code Stream} class implements {@link Iterable} to provide a simple mechanism for reading and parsing + * objects of a given type from data streamed via a {@link Reader} using a specified delimiter. + *

+ * {@code Stream} assumes that data is being pushed to the provided {@link Reader} asynchronously and utilizes a + * {@code Scanner} to block during iteration if the next object is not available. + * Iterable stream for parsing JSON and Server-Sent Events (SSE) data. + * Supports both newline-delimited JSON and SSE with optional stream termination. + * + * @param The type of objects in the stream. + */ +public final class Stream implements Iterable, Closeable { + + private static final String NEWLINE = "\n"; + private static final String DATA_PREFIX = "data:"; + + public enum StreamType { + JSON, + SSE + } + + private final Class valueType; + private final Scanner scanner; + private final StreamType streamType; + private final String messageTerminator; + private final String streamTerminator; + private final Reader sseReader; + private boolean isClosed = false; + + /** + * Constructs a new {@code Stream} with the specified value type, reader, and delimiter. + * + * @param valueType The class of the objects in the stream. + * @param reader The reader that provides the streamed data. + * @param delimiter The delimiter used to separate elements in the stream. + */ + public Stream(Class valueType, Reader reader, String delimiter) { + this.valueType = valueType; + this.scanner = new Scanner(reader).useDelimiter(delimiter); + this.streamType = StreamType.JSON; + this.messageTerminator = delimiter; + this.streamTerminator = null; + this.sseReader = null; + } + + private Stream(Class valueType, StreamType type, Reader reader, String terminator) { + this.valueType = valueType; + this.streamType = type; + if (type == StreamType.JSON) { + this.scanner = new Scanner(reader).useDelimiter(terminator); + this.messageTerminator = terminator; + this.streamTerminator = null; + this.sseReader = null; + } else { + this.scanner = null; + this.messageTerminator = NEWLINE; + this.streamTerminator = terminator; + this.sseReader = reader; + } + } + + public static Stream fromJson(Class valueType, Reader reader, String delimiter) { + return new Stream<>(valueType, reader, delimiter); + } + + public static Stream fromJson(Class valueType, Reader reader) { + return new Stream<>(valueType, reader, NEWLINE); + } + + public static Stream fromSse(Class valueType, Reader sseReader) { + return new Stream<>(valueType, StreamType.SSE, sseReader, null); + } + + public static Stream fromSse(Class valueType, Reader sseReader, String streamTerminator) { + return new Stream<>(valueType, StreamType.SSE, sseReader, streamTerminator); + } + + @Override + public void close() throws IOException { + if (!isClosed) { + isClosed = true; + if (scanner != null) { + scanner.close(); + } + if (sseReader != null) { + sseReader.close(); + } + } + } + + private boolean isStreamClosed() { + return isClosed; + } + + /** + * Returns an iterator over the elements in this stream that blocks during iteration when the next object is + * not yet available. + * + * @return An iterator that can be used to traverse the elements in the stream. + */ + @Override + public Iterator iterator() { + if (streamType == StreamType.SSE) { + return new SSEIterator(); + } else { + return new JsonIterator(); + } + } + + private final class JsonIterator implements Iterator { + + /** + * Returns {@code true} if there are more elements in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + */ + @Override + public boolean hasNext() { + if (isStreamClosed()) { + return false; + } + return scanner.hasNext(); + } + + /** + * Returns the next element in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return The next element in the stream. + * @throws NoSuchElementException If there are no more elements in the stream. + */ + @Override + public T next() { + if (isStreamClosed()) { + throw new NoSuchElementException("Stream is closed"); + } + + if (!scanner.hasNext()) { + throw new NoSuchElementException(); + } else { + try { + T parsedResponse = + ObjectMappers.JSON_MAPPER.readValue(scanner.next().trim(), valueType); + return parsedResponse; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private final class SSEIterator implements Iterator { + private Scanner sseScanner; + private T nextItem; + private boolean hasNextItem = false; + private boolean endOfStream = false; + private StringBuilder eventDataBuffer = new StringBuilder(); + private String currentEventType = null; + + private SSEIterator() { + if (sseReader != null && !isStreamClosed()) { + this.sseScanner = new Scanner(sseReader); + } else { + this.endOfStream = true; + } + } + + @Override + public boolean hasNext() { + if (isStreamClosed() || endOfStream) { + return false; + } + + if (hasNextItem) { + return true; + } + + return readNextMessage(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("No more elements in stream"); + } + + T result = nextItem; + nextItem = null; + hasNextItem = false; + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private boolean readNextMessage() { + if (sseScanner == null || isStreamClosed()) { + endOfStream = true; + return false; + } + + try { + while (sseScanner.hasNextLine()) { + String line = sseScanner.nextLine(); + + if (line.trim().isEmpty()) { + if (eventDataBuffer.length() > 0) { + try { + nextItem = ObjectMappers.JSON_MAPPER.readValue(eventDataBuffer.toString(), valueType); + hasNextItem = true; + eventDataBuffer.setLength(0); + currentEventType = null; + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse SSE event: " + parseEx.getMessage()); + eventDataBuffer.setLength(0); + currentEventType = null; + continue; + } + } + continue; + } + + if (line.startsWith(DATA_PREFIX)) { + String dataContent = line.substring(DATA_PREFIX.length()); + if (dataContent.startsWith(" ")) { + dataContent = dataContent.substring(1); + } + + if (eventDataBuffer.length() == 0 + && streamTerminator != null + && dataContent.trim().equals(streamTerminator)) { + endOfStream = true; + return false; + } + + if (eventDataBuffer.length() > 0) { + eventDataBuffer.append('\n'); + } + eventDataBuffer.append(dataContent); + } else if (line.startsWith("event:")) { + String eventValue = line.length() > 6 ? line.substring(6) : ""; + if (eventValue.startsWith(" ")) { + eventValue = eventValue.substring(1); + } + currentEventType = eventValue; + } else if (line.startsWith("id:")) { + // Event ID field (ignored) + } else if (line.startsWith("retry:")) { + // Retry field (ignored) + } else if (line.startsWith(":")) { + // Comment line (ignored) + } + } + + if (eventDataBuffer.length() > 0) { + try { + nextItem = ObjectMappers.JSON_MAPPER.readValue(eventDataBuffer.toString(), valueType); + hasNextItem = true; + eventDataBuffer.setLength(0); + currentEventType = null; + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse final SSE event: " + parseEx.getMessage()); + eventDataBuffer.setLength(0); + currentEventType = null; + } + } + + endOfStream = true; + return false; + + } catch (Exception e) { + System.err.println("Failed to parse SSE stream: " + e.getMessage()); + endOfStream = true; + return false; + } + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Suppliers.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Suppliers.java new file mode 100644 index 000000000000..037185993465 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/core/Suppliers.java @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public final class Suppliers { + private Suppliers() {} + + public static Supplier memoize(Supplier delegate) { + AtomicReference value = new AtomicReference<>(); + return () -> { + T val = value.get(); + if (val == null) { + val = value.updateAndGet(cur -> cur == null ? Objects.requireNonNull(delegate.get()) : cur); + } + return val; + }; + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/AsyncAuthClient.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/AsyncAuthClient.java new file mode 100644 index 000000000000..1c90b2f73393 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/AsyncAuthClient.java @@ -0,0 +1,36 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.resources.auth; + +import com.seed.javaOauthStagedBuilder.core.ClientOptions; +import com.seed.javaOauthStagedBuilder.core.RequestOptions; +import com.seed.javaOauthStagedBuilder.resources.auth.requests.GetTokenRequest; +import com.seed.javaOauthStagedBuilder.resources.auth.types.TokenResponse; +import java.util.concurrent.CompletableFuture; + +public class AsyncAuthClient { + protected final ClientOptions clientOptions; + + private final AsyncRawAuthClient rawClient; + + public AsyncAuthClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new AsyncRawAuthClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public AsyncRawAuthClient withRawResponse() { + return this.rawClient; + } + + public CompletableFuture getToken(GetTokenRequest request) { + return this.rawClient.getToken(request).thenApply(response -> response.body()); + } + + public CompletableFuture getToken(GetTokenRequest request, RequestOptions requestOptions) { + return this.rawClient.getToken(request, requestOptions).thenApply(response -> response.body()); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/AsyncRawAuthClient.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/AsyncRawAuthClient.java new file mode 100644 index 000000000000..01ada961fc85 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/AsyncRawAuthClient.java @@ -0,0 +1,94 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.resources.auth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.seed.javaOauthStagedBuilder.core.ClientOptions; +import com.seed.javaOauthStagedBuilder.core.MediaTypes; +import com.seed.javaOauthStagedBuilder.core.ObjectMappers; +import com.seed.javaOauthStagedBuilder.core.RequestOptions; +import com.seed.javaOauthStagedBuilder.core.SeedJavaOauthStagedBuilderApiException; +import com.seed.javaOauthStagedBuilder.core.SeedJavaOauthStagedBuilderException; +import com.seed.javaOauthStagedBuilder.core.SeedJavaOauthStagedBuilderHttpResponse; +import com.seed.javaOauthStagedBuilder.resources.auth.requests.GetTokenRequest; +import com.seed.javaOauthStagedBuilder.resources.auth.types.TokenResponse; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.NotNull; + +public class AsyncRawAuthClient { + protected final ClientOptions clientOptions; + + public AsyncRawAuthClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + public CompletableFuture> getToken(GetTokenRequest request) { + return getToken(request, null); + } + + public CompletableFuture> getToken( + GetTokenRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("token") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new SeedJavaOauthStagedBuilderException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new SeedJavaOauthStagedBuilderHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, TokenResponse.class), + response)); + return; + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedJavaOauthStagedBuilderApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (IOException e) { + future.completeExceptionally( + new SeedJavaOauthStagedBuilderException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedJavaOauthStagedBuilderException("Network error executing HTTP request", e)); + } + }); + return future; + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/AuthClient.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/AuthClient.java new file mode 100644 index 000000000000..e8768e37f3dc --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/AuthClient.java @@ -0,0 +1,35 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.resources.auth; + +import com.seed.javaOauthStagedBuilder.core.ClientOptions; +import com.seed.javaOauthStagedBuilder.core.RequestOptions; +import com.seed.javaOauthStagedBuilder.resources.auth.requests.GetTokenRequest; +import com.seed.javaOauthStagedBuilder.resources.auth.types.TokenResponse; + +public class AuthClient { + protected final ClientOptions clientOptions; + + private final RawAuthClient rawClient; + + public AuthClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new RawAuthClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public RawAuthClient withRawResponse() { + return this.rawClient; + } + + public TokenResponse getToken(GetTokenRequest request) { + return this.rawClient.getToken(request).body(); + } + + public TokenResponse getToken(GetTokenRequest request, RequestOptions requestOptions) { + return this.rawClient.getToken(request, requestOptions).body(); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/RawAuthClient.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/RawAuthClient.java new file mode 100644 index 000000000000..e001ceca9f88 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/RawAuthClient.java @@ -0,0 +1,74 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.resources.auth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.seed.javaOauthStagedBuilder.core.ClientOptions; +import com.seed.javaOauthStagedBuilder.core.MediaTypes; +import com.seed.javaOauthStagedBuilder.core.ObjectMappers; +import com.seed.javaOauthStagedBuilder.core.RequestOptions; +import com.seed.javaOauthStagedBuilder.core.SeedJavaOauthStagedBuilderApiException; +import com.seed.javaOauthStagedBuilder.core.SeedJavaOauthStagedBuilderException; +import com.seed.javaOauthStagedBuilder.core.SeedJavaOauthStagedBuilderHttpResponse; +import com.seed.javaOauthStagedBuilder.resources.auth.requests.GetTokenRequest; +import com.seed.javaOauthStagedBuilder.resources.auth.types.TokenResponse; +import java.io.IOException; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class RawAuthClient { + protected final ClientOptions clientOptions; + + public RawAuthClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + public SeedJavaOauthStagedBuilderHttpResponse getToken(GetTokenRequest request) { + return getToken(request, null); + } + + public SeedJavaOauthStagedBuilderHttpResponse getToken( + GetTokenRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("token") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new SeedJavaOauthStagedBuilderException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new SeedJavaOauthStagedBuilderHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, TokenResponse.class), response); + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedJavaOauthStagedBuilderApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (IOException e) { + throw new SeedJavaOauthStagedBuilderException("Network error executing HTTP request", e); + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/requests/GetTokenRequest.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/requests/GetTokenRequest.java new file mode 100644 index 000000000000..ffd32b8da4f7 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/requests/GetTokenRequest.java @@ -0,0 +1,189 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.resources.auth.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.javaOauthStagedBuilder.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = GetTokenRequest.Builder.class) +public final class GetTokenRequest { + private final String apiKey; + + private final String clientId; + + private final String clientSecret; + + private final Optional scope; + + private final Map additionalProperties; + + private GetTokenRequest( + String apiKey, + String clientId, + String clientSecret, + Optional scope, + Map additionalProperties) { + this.apiKey = apiKey; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.scope = scope; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("apiKey") + public String getApiKey() { + return apiKey; + } + + @JsonProperty("clientId") + public String getClientId() { + return clientId; + } + + @JsonProperty("clientSecret") + public String getClientSecret() { + return clientSecret; + } + + @JsonProperty("grantType") + public String getGrantType() { + return "client_credentials"; + } + + @JsonProperty("scope") + public Optional getScope() { + return scope; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof GetTokenRequest && equalTo((GetTokenRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(GetTokenRequest other) { + return apiKey.equals(other.apiKey) + && clientId.equals(other.clientId) + && clientSecret.equals(other.clientSecret) + && scope.equals(other.scope); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.apiKey, this.clientId, this.clientSecret, this.scope); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static ApiKeyStage builder() { + return new Builder(); + } + + public interface ApiKeyStage { + ClientIdStage apiKey(@NotNull String apiKey); + + Builder from(GetTokenRequest other); + } + + public interface ClientIdStage { + ClientSecretStage clientId(@NotNull String clientId); + } + + public interface ClientSecretStage { + _FinalStage clientSecret(@NotNull String clientSecret); + } + + public interface _FinalStage { + GetTokenRequest build(); + + _FinalStage scope(Optional scope); + + _FinalStage scope(String scope); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements ApiKeyStage, ClientIdStage, ClientSecretStage, _FinalStage { + private String apiKey; + + private String clientId; + + private String clientSecret; + + private Optional scope = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(GetTokenRequest other) { + apiKey(other.getApiKey()); + clientId(other.getClientId()); + clientSecret(other.getClientSecret()); + scope(other.getScope()); + return this; + } + + @java.lang.Override + @JsonSetter("apiKey") + public ClientIdStage apiKey(@NotNull String apiKey) { + this.apiKey = Objects.requireNonNull(apiKey, "apiKey must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("clientId") + public ClientSecretStage clientId(@NotNull String clientId) { + this.clientId = Objects.requireNonNull(clientId, "clientId must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("clientSecret") + public _FinalStage clientSecret(@NotNull String clientSecret) { + this.clientSecret = Objects.requireNonNull(clientSecret, "clientSecret must not be null"); + return this; + } + + @java.lang.Override + public _FinalStage scope(String scope) { + this.scope = Optional.ofNullable(scope); + return this; + } + + @java.lang.Override + @JsonSetter(value = "scope", nulls = Nulls.SKIP) + public _FinalStage scope(Optional scope) { + this.scope = scope; + return this; + } + + @java.lang.Override + public GetTokenRequest build() { + return new GetTokenRequest(apiKey, clientId, clientSecret, scope, additionalProperties); + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/types/TokenResponse.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/types/TokenResponse.java new file mode 100644 index 000000000000..863488bd0a7c --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/seed/javaOauthStagedBuilder/resources/auth/types/TokenResponse.java @@ -0,0 +1,124 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.resources.auth.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.javaOauthStagedBuilder.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = TokenResponse.Builder.class) +public final class TokenResponse { + private final String accessToken; + + private final int expiresIn; + + private final Map additionalProperties; + + private TokenResponse(String accessToken, int expiresIn, Map additionalProperties) { + this.accessToken = accessToken; + this.expiresIn = expiresIn; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("accessToken") + public String getAccessToken() { + return accessToken; + } + + @JsonProperty("expiresIn") + public int getExpiresIn() { + return expiresIn; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof TokenResponse && equalTo((TokenResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(TokenResponse other) { + return accessToken.equals(other.accessToken) && expiresIn == other.expiresIn; + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.accessToken, this.expiresIn); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static AccessTokenStage builder() { + return new Builder(); + } + + public interface AccessTokenStage { + ExpiresInStage accessToken(@NotNull String accessToken); + + Builder from(TokenResponse other); + } + + public interface ExpiresInStage { + _FinalStage expiresIn(int expiresIn); + } + + public interface _FinalStage { + TokenResponse build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements AccessTokenStage, ExpiresInStage, _FinalStage { + private String accessToken; + + private int expiresIn; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(TokenResponse other) { + accessToken(other.getAccessToken()); + expiresIn(other.getExpiresIn()); + return this; + } + + @java.lang.Override + @JsonSetter("accessToken") + public ExpiresInStage accessToken(@NotNull String accessToken) { + this.accessToken = Objects.requireNonNull(accessToken, "accessToken must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("expiresIn") + public _FinalStage expiresIn(int expiresIn) { + this.expiresIn = expiresIn; + return this; + } + + @java.lang.Override + public TokenResponse build() { + return new TokenResponse(accessToken, expiresIn, additionalProperties); + } + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/snippets/Example0.java b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/snippets/Example0.java new file mode 100644 index 000000000000..5ff6d07d5b0e --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/main/java/com/snippets/Example0.java @@ -0,0 +1,22 @@ +package com.snippets; + +import com.seed.javaOauthStagedBuilder.SeedJavaOauthStagedBuilderClient; +import com.seed.javaOauthStagedBuilder.resources.auth.requests.GetTokenRequest; + +public class Example0 { + public static void main(String[] args) { + SeedJavaOauthStagedBuilderClient client = SeedJavaOauthStagedBuilderClient.builder() + .clientId("") + .clientSecret("") + .url("https://api.fern.com") + .build(); + + client.auth() + .getToken(GetTokenRequest.builder() + .apiKey("apiKey") + .clientId("clientId") + .clientSecret("clientSecret") + .scope("scope") + .build()); + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/test/java/com/seed/javaOauthStagedBuilder/StreamTest.java b/seed/java-sdk/java-oauth-staged-builder/default/src/test/java/com/seed/javaOauthStagedBuilder/StreamTest.java new file mode 100644 index 000000000000..36f07d7990a3 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/test/java/com/seed/javaOauthStagedBuilder/StreamTest.java @@ -0,0 +1,97 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder; + +import static org.junit.jupiter.api.Assertions.*; + +import com.seed.javaOauthStagedBuilder.core.ObjectMappers; +import com.seed.javaOauthStagedBuilder.core.Stream; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +public final class StreamTest { + @Test + public void testJsonStream() { + List> messages = + Arrays.asList(createMap("message", "hello"), createMap("message", "world")); + List jsonStrings = messages.stream().map(StreamTest::mapToJson).collect(Collectors.toList()); + String input = String.join("\n", jsonStrings); + StringReader jsonInput = new StringReader(input); + Stream jsonStream = Stream.fromJson(Map.class, jsonInput); + int expectedMessages = 2; + int actualMessages = 0; + for (Map jsonObject : jsonStream) { + actualMessages++; + assertTrue(jsonObject.containsKey("message")); + } + assertEquals(expectedMessages, actualMessages); + } + + @Test + public void testSseStream() { + List> events = Arrays.asList(createMap("event", "start"), createMap("event", "end")); + List sseStrings = events.stream().map(StreamTest::mapToSse).collect(Collectors.toList()); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSse(Map.class, sseInput); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + assertTrue(eventData.containsKey("event")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testSseStreamWithTerminator() { + List> events = Arrays.asList(createMap("message", "first"), createMap("message", "second")); + List sseStrings = + new ArrayList<>(events.stream().map(StreamTest::mapToSse).collect(Collectors.toList())); + sseStrings.add("data: [DONE]"); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSse(Map.class, sseInput, "[DONE]"); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + assertTrue(eventData.containsKey("message")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testStreamResourceManagement() throws IOException { + StringReader testInput = new StringReader("{\"test\":\"data\"}"); + Stream testStream = Stream.fromJson(Map.class, testInput); + testStream.close(); + assertFalse(testStream.iterator().hasNext()); + } + + private static String mapToJson(Map map) { + try { + return ObjectMappers.JSON_MAPPER.writeValueAsString(map); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String mapToSse(Map map) { + return "data: " + mapToJson(map); + } + + private static Map createMap(String key, String value) { + Map map = new HashMap<>(); + map.put(key, value); + return map; + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/test/java/com/seed/javaOauthStagedBuilder/TestClient.java b/seed/java-sdk/java-oauth-staged-builder/default/src/test/java/com/seed/javaOauthStagedBuilder/TestClient.java new file mode 100644 index 000000000000..a9e15c250ef4 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/test/java/com/seed/javaOauthStagedBuilder/TestClient.java @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder; + +public final class TestClient { + public void test() { + // Add tests here and mark this file in .fernignore + assert true; + } +} diff --git a/seed/java-sdk/java-oauth-staged-builder/default/src/test/java/com/seed/javaOauthStagedBuilder/core/QueryStringMapperTest.java b/seed/java-sdk/java-oauth-staged-builder/default/src/test/java/com/seed/javaOauthStagedBuilder/core/QueryStringMapperTest.java new file mode 100644 index 000000000000..55fcab8d02e1 --- /dev/null +++ b/seed/java-sdk/java-oauth-staged-builder/default/src/test/java/com/seed/javaOauthStagedBuilder/core/QueryStringMapperTest.java @@ -0,0 +1,339 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaOauthStagedBuilder.core; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.HttpUrl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public final class QueryStringMapperTest { + @Test + public void testObjectWithQuotedString_indexedArrays() { + Map map = new HashMap() { + { + put("hello", "\"world\""); + } + }; + + String expectedQueryString = "withquoted%5Bhello%5D=%22world%22"; + + String actualQueryString = queryString( + new HashMap() { + { + put("withquoted", map); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithQuotedString_arraysAsRepeats() { + Map map = new HashMap() { + { + put("hello", "\"world\""); + } + }; + + String expectedQueryString = "withquoted%5Bhello%5D=%22world%22"; + + String actualQueryString = queryString( + new HashMap() { + { + put("withquoted", map); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObject_indexedArrays() { + Map map = new HashMap() { + { + put("foo", "bar"); + put("baz", "qux"); + } + }; + + String expectedQueryString = "metadata%5Bfoo%5D=bar&metadata%5Bbaz%5D=qux"; + + String actualQueryString = queryString( + new HashMap() { + { + put("metadata", map); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObject_arraysAsRepeats() { + Map map = new HashMap() { + { + put("foo", "bar"); + put("baz", "qux"); + } + }; + + String expectedQueryString = "metadata%5Bfoo%5D=bar&metadata%5Bbaz%5D=qux"; + + String actualQueryString = queryString( + new HashMap() { + { + put("metadata", map); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testNestedObject_indexedArrays() { + Map> nestedMap = new HashMap>() { + { + put("mapkey1", new HashMap() { + { + put("mapkey1mapkey1", "mapkey1mapkey1value"); + put("mapkey1mapkey2", "mapkey1mapkey2value"); + } + }); + put("mapkey2", new HashMap() { + { + put("mapkey2mapkey1", "mapkey2mapkey1value"); + } + }); + } + }; + + String expectedQueryString = + "nested%5Bmapkey2%5D%5Bmapkey2mapkey1%5D=mapkey2mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey1" + + "%5D=mapkey1mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey2%5D=mapkey1mapkey2value"; + + String actualQueryString = queryString( + new HashMap() { + { + put("nested", nestedMap); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testNestedObject_arraysAsRepeats() { + Map> nestedMap = new HashMap>() { + { + put("mapkey1", new HashMap() { + { + put("mapkey1mapkey1", "mapkey1mapkey1value"); + put("mapkey1mapkey2", "mapkey1mapkey2value"); + } + }); + put("mapkey2", new HashMap() { + { + put("mapkey2mapkey1", "mapkey2mapkey1value"); + } + }); + } + }; + + String expectedQueryString = + "nested%5Bmapkey2%5D%5Bmapkey2mapkey1%5D=mapkey2mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey1" + + "%5D=mapkey1mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey2%5D=mapkey1mapkey2value"; + + String actualQueryString = queryString( + new HashMap() { + { + put("nested", nestedMap); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testDateTime_indexedArrays() { + OffsetDateTime dateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1740412107L), ZoneId.of("America/New_York")); + + String expectedQueryString = "datetime=2025-02-24T10%3A48%3A27-05%3A00"; + + String actualQueryString = queryString( + new HashMap() { + { + put("datetime", dateTime); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testDateTime_arraysAsRepeats() { + OffsetDateTime dateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1740412107L), ZoneId.of("America/New_York")); + + String expectedQueryString = "datetime=2025-02-24T10%3A48%3A27-05%3A00"; + + String actualQueryString = queryString( + new HashMap() { + { + put("datetime", dateTime); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectArray_indexedArrays() { + List> mapArray = new ArrayList>() { + { + add(new HashMap() { + { + put("key", "hello"); + put("value", "world"); + } + }); + add(new HashMap() { + { + put("key", "foo"); + put("value", "bar"); + } + }); + add(new HashMap<>()); + } + }; + + String expectedQueryString = "objects%5B0%5D%5Bvalue%5D=world&objects%5B0%5D%5Bkey%5D=hello&objects%5B1%5D" + + "%5Bvalue%5D=bar&objects%5B1%5D%5Bkey%5D=foo"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objects", mapArray); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectArray_arraysAsRepeats() { + List> mapArray = new ArrayList>() { + { + add(new HashMap() { + { + put("key", "hello"); + put("value", "world"); + } + }); + add(new HashMap() { + { + put("key", "foo"); + put("value", "bar"); + } + }); + add(new HashMap<>()); + } + }; + + String expectedQueryString = + "objects%5Bvalue%5D=world&objects%5Bkey%5D=hello&objects%5Bvalue" + "%5D=bar&objects%5Bkey%5D=foo"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objects", mapArray); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithArray_indexedArrays() { + Map objectWithArray = new HashMap() { + { + put("id", "abc123"); + put("contactIds", new ArrayList() { + { + add("id1"); + add("id2"); + add("id3"); + } + }); + } + }; + + String expectedQueryString = + "objectwitharray%5Bid%5D=abc123&objectwitharray%5BcontactIds%5D%5B0%5D=id1&objectwitharray" + + "%5BcontactIds%5D%5B1%5D=id2&objectwitharray%5BcontactIds%5D%5B2%5D=id3"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objectwitharray", objectWithArray); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithArray_arraysAsRepeats() { + Map objectWithArray = new HashMap() { + { + put("id", "abc123"); + put("contactIds", new ArrayList() { + { + add("id1"); + add("id2"); + add("id3"); + } + }); + } + }; + + String expectedQueryString = "objectwitharray%5Bid%5D=abc123&objectwitharray%5BcontactIds" + + "%5D=id1&objectwitharray%5BcontactIds%5D=id2&objectwitharray%5BcontactIds%5D=id3"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objectwitharray", objectWithArray); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + private static String queryString(Map params, boolean arraysAsRepeats) { + HttpUrl.Builder httpUrl = HttpUrl.parse("http://www.fakewebsite.com/").newBuilder(); + params.forEach((paramName, paramValue) -> + QueryStringMapper.addQueryParameter(httpUrl, paramName, paramValue, arraysAsRepeats)); + return httpUrl.build().encodedQuery(); + } +} diff --git a/seed/java-sdk/seed.yml b/seed/java-sdk/seed.yml index fc12f3c005d1..2452debf8d9d 100644 --- a/seed/java-sdk/seed.yml +++ b/seed/java-sdk/seed.yml @@ -290,6 +290,8 @@ fixtures: - customConfig: enable-wire-tests: true outputFolder: streaming + java-oauth-staged-builder: + - outputFolder: default scripts: - image: fernapi/java-seed commands: diff --git a/test-definitions/fern/apis/java-oauth-staged-builder/definition/api.yml b/test-definitions/fern/apis/java-oauth-staged-builder/definition/api.yml new file mode 100644 index 000000000000..728e2565f08d --- /dev/null +++ b/test-definitions/fern/apis/java-oauth-staged-builder/definition/api.yml @@ -0,0 +1,17 @@ +name: java-oauth-staged-builder +imports: + auth: auth.yml + +auth: OAuthScheme +auth-schemes: + OAuthScheme: + scheme: oauth + type: client-credentials + get-token: + endpoint: auth.getToken + request-properties: + client-id: $request.clientId + client-secret: $request.clientSecret + response-properties: + access-token: $response.accessToken + expires-in: $response.expiresIn diff --git a/test-definitions/fern/apis/java-oauth-staged-builder/definition/auth.yml b/test-definitions/fern/apis/java-oauth-staged-builder/definition/auth.yml new file mode 100644 index 000000000000..f66ab5ca5135 --- /dev/null +++ b/test-definitions/fern/apis/java-oauth-staged-builder/definition/auth.yml @@ -0,0 +1,23 @@ +types: + TokenResponse: + properties: + accessToken: string + expiresIn: integer + +service: + auth: false + base-path: / + endpoints: + getToken: + path: /token + method: POST + request: + name: GetTokenRequest + body: + properties: + apiKey: string + clientId: string + clientSecret: string + grantType: literal<"client_credentials"> + scope: optional + response: TokenResponse diff --git a/test-definitions/fern/apis/java-oauth-staged-builder/generators.yml b/test-definitions/fern/apis/java-oauth-staged-builder/generators.yml new file mode 100644 index 000000000000..5c0b96dcfafc --- /dev/null +++ b/test-definitions/fern/apis/java-oauth-staged-builder/generators.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +# This test fixture is Java-specific and tests that OAuthTokenSupplier +# generates builder calls in the correct staged builder order when +# the token endpoint uses a referenced request type (not inlined). +groups: {}