Skip to content

Commit bf3adf5

Browse files
authored
update Java custom code with new self() implemenation (#362)
1 parent 6187084 commit bf3adf5

File tree

1 file changed

+87
-76
lines changed

1 file changed

+87
-76
lines changed

fern/products/sdks/overview/java/custom-code.mdx

Lines changed: 87 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -104,106 +104,111 @@ To get started adding custom code:
104104

105105
## Adding custom client configuration
106106

107-
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.
107+
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.
108108

109109
Common use cases include:
110-
- **Dynamic URL construction**: Replace placeholders with runtime values (e.g., `https://api.${DEV_NAMESPACE}.example.com`)
110+
- **Dynamic URL construction**: Replace placeholders with runtime values (e.g., `https://api.${TENANT}.example.com`)
111111
- **Custom authentication**: Implement complex auth flows beyond basic token authentication
112112
- **Request transformation**: Add custom headers or modify requests globally
113-
114-
### How it works
115-
116-
Generated builders use protected methods that can be overridden:
117-
118-
```java
119-
public class BaseApiClientBuilder {
120-
protected ClientOptions buildClientOptions() {
121-
ClientOptions.Builder builder = ClientOptions.builder();
122-
setEnvironment(builder);
123-
setAuthentication(builder); // Only if API has auth
124-
setCustomHeaders(builder); // Only if API defines headers
125-
setVariables(builder); // Only if API has variables
126-
setHttpClient(builder);
127-
setTimeouts(builder);
128-
setRetries(builder);
129-
setAdditional(builder);
130-
return builder.build();
131-
}
132-
}
133-
```
113+
- **Multi-tenant support**: Add tenant-specific configuration and headers
134114

135115
<Steps>
136116

137-
### Create a custom client class
117+
### Enable extensible builders
138118

139-
Extend the generated base client:
119+
Add the flag to your `generators.yml`:
140120

141-
```java title="src/main/java/com/example/MyClient.java"
142-
package com.example;
121+
```yaml {7} title="generators.yml"
122+
groups:
123+
local:
124+
generators:
125+
- name: fernapi/fern-java-sdk
126+
version: 2.39.6
127+
config:
128+
enable-extensible-builders: true
129+
```
143130
144-
import com.example.api.BaseClient;
145-
import com.example.api.core.ClientOptions;
131+
### How it works
146132
147-
public class MyClient extends BaseClient {
148-
public MyClient(ClientOptions clientOptions) {
149-
super(clientOptions);
150-
}
133+
Generated builders use the self-type pattern for type-safe method chaining:
151134
152-
public static MyClientBuilder builder() {
153-
return new MyClientBuilder();
135+
```java
136+
public abstract class BaseClientBuilder<T extends BaseClientBuilder<T>> {
137+
protected abstract T self();
138+
139+
public T token(String token) {
140+
return self(); // Returns your custom type, not BaseClientBuilder
154141
}
155142
}
156143
```
157144

158-
### Create a custom builder class
145+
### Create a custom builder
159146

160-
Override methods to customize behavior:
147+
Extend the generated builder:
161148

162-
```java title="src/main/java/com/example/MyClientBuilder.java"
163-
package com.example;
164-
165-
import com.example.api.BaseClient.BaseClientBuilder;
166-
import com.example.api.core.ClientOptions;
167-
import com.example.api.core.Environment;
168-
169-
public class MyClientBuilder extends BaseClientBuilder {
149+
```java title="src/main/java/com/example/CustomApiBuilder.java"
150+
public class CustomApiBuilder extends BaseClientBuilder<CustomApiBuilder> {
151+
@Override
152+
protected CustomApiBuilder self() {
153+
return this;
154+
}
170155

171156
@Override
172157
protected void setEnvironment(ClientOptions.Builder builder) {
158+
// Customize environment URL
173159
String url = this.environment.getUrl();
174160
String expandedUrl = expandEnvironmentVariables(url);
175161
builder.environment(Environment.custom(expandedUrl));
176162
}
177163

178164
@Override
179165
protected void setAdditional(ClientOptions.Builder builder) {
166+
// Add custom headers
180167
builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
181168
}
182-
183-
@Override
184-
public MyClient build() {
185-
return new MyClient(buildClientOptions());
186-
}
187169
}
188170
```
189171

172+
### Use your custom builder
173+
174+
```java
175+
BaseClient client = new CustomApiBuilder()
176+
.token("my-token") // returns CustomApiBuilder
177+
.tenantId("tenant-123") // returns CustomApiBuilder
178+
.timeout(30) // returns CustomApiBuilder
179+
.build();
180+
181+
client.users().list();
182+
```
183+
190184
### Update `.fernignore`
191185

192-
By adding these two files to `.fernignore`, fern will not update them on new generations.
186+
Add your custom builder to `.fernignore` so Fern won't overwrite it:
193187

194188
```diff title=".fernignore"
195-
+ src/main/java/com/example/MyClient.java
196-
+ src/main/java/com/example/MyClientBuilder.java
189+
+ src/main/java/com/example/CustomApiBuilder.java
197190
```
198191

199192
</Steps>
200193

194+
### Default implementation
195+
196+
If you don't need to extend the builder, use the provided `Impl` class:
197+
198+
```java
199+
BaseClient client = BaseClientBuilder.Impl()
200+
.token("my-token")
201+
.timeout(30)
202+
.build();
203+
```
204+
201205
### Method reference
202206

203207
Each method serves a specific purpose and is only generated when needed:
204208

205209
| Method | Purpose | Available When |
206210
|--------|---------|----------------|
211+
| `self()` | Returns the concrete builder type for chaining | Always (abstract) |
207212
| `setEnvironment(builder)` | Customize environment/URL configuration | Always |
208213
| `setAuthentication(builder)` | Modify or add authentication | Only if API has auth |
209214
| `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:
213218
| `setRetries(builder)` | Modify retry settings | Always |
214219
| `setAdditional(builder)` | Final extension point for any custom configuration | Always |
215220
| `validateConfiguration()` | Add custom validation logic | Always |
216-
| `buildClientOptions()` | Orchestrates all configuration methods (rarely need to override) | Always |
217221

218222
### Common patterns
219223

220224
<Accordion title="Multi-tenant URLs">
221225
```java
222226
@Override
223227
protected void setEnvironment(ClientOptions.Builder builder) {
224-
String baseUrl = this.environment.getUrl();
225-
String tenantUrl = baseUrl.replace("/api/", "/tenants/" + tenantId + "/");
226-
builder.environment(Environment.custom(tenantUrl));
228+
String url = this.environment.getUrl()
229+
.replace("/api/", "/tenants/" + tenantId + "/");
230+
builder.environment(Environment.custom(url));
227231
}
228232
```
229233
</Accordion>
@@ -232,47 +236,54 @@ protected void setEnvironment(ClientOptions.Builder builder) {
232236
```java
233237
@Override
234238
protected void setAuthentication(ClientOptions.Builder builder) {
235-
builder.addHeader("Authorization", () ->
239+
super.setAuthentication(builder); // Keep existing auth
240+
builder.addHeader("Authorization", () ->
236241
"Bearer " + tokenProvider.getAccessToken()
237242
);
238243
}
239244
```
240245
</Accordion>
241246

242-
<Accordion title="Request tracking and monitoring">
247+
<Accordion title="Environment variable expansion">
243248
```java
244249
@Override
245-
protected void setAdditional(ClientOptions.Builder builder) {
246-
// Add request tracking
247-
builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
248-
builder.addHeader("X-Client-Version", "1.0.0");
250+
protected void setEnvironment(ClientOptions.Builder builder) {
251+
String url = this.environment.getUrl();
252+
// Replace ${VAR_NAME} with environment variables
253+
Pattern pattern = Pattern.compile("\\$\\{([^}]+)\\}");
254+
Matcher matcher = pattern.matcher(url);
255+
StringBuffer result = new StringBuffer();
249256

250-
// Add feature flags
251-
if (FeatureFlags.isEnabled("new-algorithm")) {
252-
builder.addHeader("X-Feature-Flag", "new-algorithm");
257+
while (matcher.find()) {
258+
String envVar = System.getenv(matcher.group(1));
259+
matcher.appendReplacement(result,
260+
envVar != null ? envVar : matcher.group(0));
253261
}
262+
matcher.appendTail(result);
263+
264+
builder.environment(Environment.custom(result.toString()));
254265
}
255266
```
256267
</Accordion>
257268

258-
<Accordion title="Advanced HTTP client configuration">
269+
<Accordion title="Request tracking">
259270
```java
260271
@Override
261-
protected void setHttpClient(ClientOptions.Builder builder) {
262-
OkHttpClient customClient = new OkHttpClient.Builder()
263-
.connectTimeout(30, TimeUnit.SECONDS)
264-
.readTimeout(60, TimeUnit.SECONDS)
265-
.addInterceptor(new RetryInterceptor())
266-
.connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES))
267-
.build();
268-
builder.httpClient(customClient);
272+
protected void setAdditional(ClientOptions.Builder builder) {
273+
builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
274+
builder.addHeader("X-Tenant-ID", this.tenantId);
275+
276+
if (FeatureFlags.isEnabled("new-feature")) {
277+
builder.addHeader("X-Feature-Flag", "new-feature");
278+
}
269279
}
270280
```
271281
</Accordion>
272282

273283
### Requirements
274284

275-
- **Fern Java SDK version**: 2.39.1 or later
285+
- **Fern Java SDK version**: 2.39.6 or later
286+
- **Configuration**: `enable-extensible-builders: true` in `generators.yml`
276287

277288
## Adding custom dependencies
278289

0 commit comments

Comments
 (0)