-
Notifications
You must be signed in to change notification settings - Fork 3
update Java custom code with new self() implemenation #362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,106 +104,111 @@ 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 | ||
|
|
||
| <Steps> | ||
|
|
||
| ### 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<T extends BaseClientBuilder<T>> { | ||
| 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<CustomApiBuilder> { | ||
| @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)); | ||
| } | ||
|
|
||
| @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 | ||
| ``` | ||
|
|
||
| </Steps> | ||
|
|
||
| ### Default implementation | ||
|
|
||
| If you don't need to extend the builder, use the provided `Impl` class: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think it'd be pretty clear to someone when they would or wouldn't need to extend the builder? If not, you could add another sentence describing a typical use case for someone who doesn't need to extend the builder.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea I think so actually! If they want base client options (most customers just rely on the one that is generated) it would make sense. |
||
|
|
||
| ```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,17 +218,16 @@ 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 | ||
|
|
||
| <Accordion title="Multi-tenant URLs"> | ||
| ```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)); | ||
| } | ||
| ``` | ||
| </Accordion> | ||
|
|
@@ -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() | ||
| ); | ||
| } | ||
| ``` | ||
| </Accordion> | ||
|
|
||
| <Accordion title="Request tracking and monitoring"> | ||
| <Accordion title="Environment variable expansion"> | ||
| ```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())); | ||
| } | ||
| ``` | ||
| </Accordion> | ||
|
|
||
| <Accordion title="Advanced HTTP client configuration"> | ||
| <Accordion title="Request tracking"> | ||
| ```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"); | ||
| } | ||
| } | ||
| ``` | ||
| </Accordion> | ||
|
|
||
| ### 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` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be great if you could add a new entry with a brief description in the Java Configuration page for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do! |
||
|
|
||
| ## Adding custom dependencies | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think most people reading this page would understand what "self-type pattern" means? I'd consider wording this differently if not. (Feel free to ignore this if you think it's clear the way it is, I'm no Java expert!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so! If the users are comfortable with Java they should know what this provides.