From fe321cdc1ed176213048f1d877494c916b5d1e38 Mon Sep 17 00:00:00 2001 From: Tanmay Singh Date: Tue, 5 Aug 2025 17:56:06 -0400 Subject: [PATCH] update Java custom code with new self() implemenation --- .../sdks/overview/java/custom-code.mdx | 163 ++++++++++-------- 1 file changed, 87 insertions(+), 76 deletions(-) diff --git a/fern/products/sdks/overview/java/custom-code.mdx b/fern/products/sdks/overview/java/custom-code.mdx index 18e96978c..af0efd1d8 100644 --- a/fern/products/sdks/overview/java/custom-code.mdx +++ b/fern/products/sdks/overview/java/custom-code.mdx @@ -104,72 +104,58 @@ To get started adding custom code: ## Adding custom client configuration -The Java SDK generator supports builder extensibility through a template method pattern. By extending the generated builder classes and overriding protected methods, you can customize how your SDK client is configured without modifying generated code. +The Java SDK generator supports builder extensibility through an opt-in self-type pattern. When enabled via the `enable-extensible-builders` flag, generated builders can be extended while maintaining type safety during method chaining. Common use cases include: -- **Dynamic URL construction**: Replace placeholders with runtime values (e.g., `https://api.${DEV_NAMESPACE}.example.com`) +- **Dynamic URL construction**: Replace placeholders with runtime values (e.g., `https://api.${TENANT}.example.com`) - **Custom authentication**: Implement complex auth flows beyond basic token authentication - **Request transformation**: Add custom headers or modify requests globally - -### How it works - -Generated builders use protected methods that can be overridden: - -```java -public class BaseApiClientBuilder { - protected ClientOptions buildClientOptions() { - ClientOptions.Builder builder = ClientOptions.builder(); - setEnvironment(builder); - setAuthentication(builder); // Only if API has auth - setCustomHeaders(builder); // Only if API defines headers - setVariables(builder); // Only if API has variables - setHttpClient(builder); - setTimeouts(builder); - setRetries(builder); - setAdditional(builder); - return builder.build(); - } -} -``` +- **Multi-tenant support**: Add tenant-specific configuration and headers -### Create a custom client class +### Enable extensible builders -Extend the generated base client: +Add the flag to your `generators.yml`: -```java title="src/main/java/com/example/MyClient.java" -package com.example; +```yaml {7} title="generators.yml" +groups: + local: + generators: + - name: fernapi/fern-java-sdk + version: 2.39.6 + config: + enable-extensible-builders: true +``` -import com.example.api.BaseClient; -import com.example.api.core.ClientOptions; +### How it works -public class MyClient extends BaseClient { - public MyClient(ClientOptions clientOptions) { - super(clientOptions); - } +Generated builders use the self-type pattern for type-safe method chaining: - public static MyClientBuilder builder() { - return new MyClientBuilder(); +```java +public abstract class BaseClientBuilder> { + protected abstract T self(); + + public T token(String token) { + return self(); // Returns your custom type, not BaseClientBuilder } } ``` -### Create a custom builder class +### Create a custom builder -Override methods to customize behavior: +Extend the generated builder: -```java title="src/main/java/com/example/MyClientBuilder.java" -package com.example; - -import com.example.api.BaseClient.BaseClientBuilder; -import com.example.api.core.ClientOptions; -import com.example.api.core.Environment; - -public class MyClientBuilder extends BaseClientBuilder { +```java title="src/main/java/com/example/CustomApiBuilder.java" +public class CustomApiBuilder extends BaseClientBuilder { + @Override + protected CustomApiBuilder self() { + return this; + } @Override protected void setEnvironment(ClientOptions.Builder builder) { + // Customize environment URL String url = this.environment.getUrl(); String expandedUrl = expandEnvironmentVariables(url); builder.environment(Environment.custom(expandedUrl)); @@ -177,33 +163,52 @@ public class MyClientBuilder extends BaseClientBuilder { @Override protected void setAdditional(ClientOptions.Builder builder) { + // Add custom headers builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString()); } - - @Override - public MyClient build() { - return new MyClient(buildClientOptions()); - } } ``` +### Use your custom builder + +```java +BaseClient client = new CustomApiBuilder() + .token("my-token") // returns CustomApiBuilder + .tenantId("tenant-123") // returns CustomApiBuilder + .timeout(30) // returns CustomApiBuilder + .build(); + +client.users().list(); +``` + ### Update `.fernignore` -By adding these two files to `.fernignore`, fern will not update them on new generations. +Add your custom builder to `.fernignore` so Fern won't overwrite it: ```diff title=".fernignore" -+ src/main/java/com/example/MyClient.java -+ src/main/java/com/example/MyClientBuilder.java ++ src/main/java/com/example/CustomApiBuilder.java ``` +### Default implementation + +If you don't need to extend the builder, use the provided `Impl` class: + +```java +BaseClient client = BaseClientBuilder.Impl() + .token("my-token") + .timeout(30) + .build(); +``` + ### Method reference Each method serves a specific purpose and is only generated when needed: | Method | Purpose | Available When | |--------|---------|----------------| +| `self()` | Returns the concrete builder type for chaining | Always (abstract) | | `setEnvironment(builder)` | Customize environment/URL configuration | Always | | `setAuthentication(builder)` | Modify or add authentication | Only if API has auth | | `setCustomHeaders(builder)` | Add custom headers defined in API spec | Only if API defines headers | @@ -213,7 +218,6 @@ Each method serves a specific purpose and is only generated when needed: | `setRetries(builder)` | Modify retry settings | Always | | `setAdditional(builder)` | Final extension point for any custom configuration | Always | | `validateConfiguration()` | Add custom validation logic | Always | -| `buildClientOptions()` | Orchestrates all configuration methods (rarely need to override) | Always | ### Common patterns @@ -221,9 +225,9 @@ Each method serves a specific purpose and is only generated when needed: ```java @Override protected void setEnvironment(ClientOptions.Builder builder) { - String baseUrl = this.environment.getUrl(); - String tenantUrl = baseUrl.replace("/api/", "/tenants/" + tenantId + "/"); - builder.environment(Environment.custom(tenantUrl)); + String url = this.environment.getUrl() + .replace("/api/", "/tenants/" + tenantId + "/"); + builder.environment(Environment.custom(url)); } ``` @@ -232,47 +236,54 @@ protected void setEnvironment(ClientOptions.Builder builder) { ```java @Override protected void setAuthentication(ClientOptions.Builder builder) { - builder.addHeader("Authorization", () -> + super.setAuthentication(builder); // Keep existing auth + builder.addHeader("Authorization", () -> "Bearer " + tokenProvider.getAccessToken() ); } ``` - + ```java @Override -protected void setAdditional(ClientOptions.Builder builder) { - // Add request tracking - builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString()); - builder.addHeader("X-Client-Version", "1.0.0"); +protected void setEnvironment(ClientOptions.Builder builder) { + String url = this.environment.getUrl(); + // Replace ${VAR_NAME} with environment variables + Pattern pattern = Pattern.compile("\\$\\{([^}]+)\\}"); + Matcher matcher = pattern.matcher(url); + StringBuffer result = new StringBuffer(); - // Add feature flags - if (FeatureFlags.isEnabled("new-algorithm")) { - builder.addHeader("X-Feature-Flag", "new-algorithm"); + while (matcher.find()) { + String envVar = System.getenv(matcher.group(1)); + matcher.appendReplacement(result, + envVar != null ? envVar : matcher.group(0)); } + matcher.appendTail(result); + + builder.environment(Environment.custom(result.toString())); } ``` - + ```java @Override -protected void setHttpClient(ClientOptions.Builder builder) { - OkHttpClient customClient = new OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .addInterceptor(new RetryInterceptor()) - .connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES)) - .build(); - builder.httpClient(customClient); +protected void setAdditional(ClientOptions.Builder builder) { + builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString()); + builder.addHeader("X-Tenant-ID", this.tenantId); + + if (FeatureFlags.isEnabled("new-feature")) { + builder.addHeader("X-Feature-Flag", "new-feature"); + } } ``` ### Requirements -- **Fern Java SDK version**: 2.39.1 or later +- **Fern Java SDK version**: 2.39.6 or later +- **Configuration**: `enable-extensible-builders: true` in `generators.yml` ## Adding custom dependencies