Skip to content

Commit ff9223b

Browse files
tjb9dcclaude
andcommitted
feat(java): generate UriPage/PathPage utilities and conditionally include pagination files
- Add UriPage.java and PathPage.java template resources for URI/path pagination - Rewrite visitUriOrPathPagination to generate UriPage.create()/PathPage.create() calls instead of inline AtomicReference lambda pattern - Conditionally include UriPage, PathPage, CustomPager, AsyncCustomPager, and BiDirectionalPage only when the IR has endpoints using those pagination types - Pass exception class names to PaginationCoreGenerator for proper error handling in generated pagination utilities Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0b79b2f commit ff9223b

File tree

13 files changed

+395
-738
lines changed

13 files changed

+395
-738
lines changed

generators/java/generator-utils/src/main/java/com/fern/java/generators/PaginationCoreGenerator.java

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,35 @@
1818

1919
import static com.fern.java.GeneratorLogging.logError;
2020

21+
import com.fern.ir.model.http.HttpEndpoint;
22+
import com.fern.ir.model.http.HttpService;
23+
import com.fern.ir.model.http.Pagination;
2124
import com.fern.java.AbstractGeneratorContext;
2225
import com.fern.java.DefaultGeneratorExecClient;
2326
import com.fern.java.output.GeneratedFile;
2427
import com.fern.java.output.GeneratedResourcesJavaFile;
2528
import java.io.IOException;
2629
import java.io.InputStream;
2730
import java.nio.charset.StandardCharsets;
31+
import java.util.ArrayList;
2832
import java.util.List;
2933
import java.util.stream.Collectors;
3034

3135
public final class PaginationCoreGenerator extends AbstractFilesGenerator {
3236
public static final String GET_MODULE_METHOD_NAME = "getModule";
3337
private final DefaultGeneratorExecClient generatorExecClient;
38+
private final String apiExceptionSimpleName;
39+
private final String baseExceptionSimpleName;
3440

3541
public PaginationCoreGenerator(
36-
AbstractGeneratorContext<?, ?> generatorContext, DefaultGeneratorExecClient generatorExecClient) {
42+
AbstractGeneratorContext<?, ?> generatorContext,
43+
DefaultGeneratorExecClient generatorExecClient,
44+
String apiExceptionSimpleName,
45+
String baseExceptionSimpleName) {
3746
super(generatorContext);
3847
this.generatorExecClient = generatorExecClient;
48+
this.apiExceptionSimpleName = apiExceptionSimpleName;
49+
this.baseExceptionSimpleName = baseExceptionSimpleName;
3950
}
4051

4152
@Override
@@ -56,8 +67,37 @@ public List<GeneratedFile> generateFiles() {
5667
return List.of();
5768
}
5869

59-
List<String> fileNames = List.of(
60-
"BasePage", "SyncPage", "SyncPagingIterable", "BiDirectionalPage", "CustomPager", "AsyncCustomPager");
70+
// Scan IR to determine which pagination types are in use
71+
boolean hasCustomPagination = false;
72+
boolean hasUriPagination = false;
73+
boolean hasPathPagination = false;
74+
for (HttpService service : generatorContext.getIr().getServices().values()) {
75+
for (HttpEndpoint endpoint : service.getEndpoints()) {
76+
if (endpoint.getPagination().isPresent()) {
77+
Pagination pagination = endpoint.getPagination().get();
78+
if (pagination.isCustom()) {
79+
hasCustomPagination = true;
80+
} else if (pagination.isUri()) {
81+
hasUriPagination = true;
82+
} else if (pagination.isPath()) {
83+
hasPathPagination = true;
84+
}
85+
}
86+
}
87+
}
88+
89+
List<String> fileNames = new ArrayList<>(List.of("BasePage", "SyncPage", "SyncPagingIterable"));
90+
if (hasCustomPagination) {
91+
fileNames.add("BiDirectionalPage");
92+
fileNames.add("CustomPager");
93+
fileNames.add("AsyncCustomPager");
94+
}
95+
if (hasUriPagination) {
96+
fileNames.add("UriPage");
97+
}
98+
if (hasPathPagination) {
99+
fileNames.add("PathPage");
100+
}
61101

62102
String corePackage = generatorContext.getPoetClassNameFactory().getCorePackage();
63103

@@ -70,16 +110,29 @@ public List<GeneratedFile> generateFiles() {
70110
}
71111
String contents = new String(is.readAllBytes(), StandardCharsets.UTF_8);
72112

73-
// Add ClientOptions import for CustomPager and AsyncCustomPager
113+
// Replace core package placeholders in templates
74114
if (fileName.equals("CustomPager") || fileName.equals("AsyncCustomPager")) {
75115
String clientOptionsImport = "import " + corePackage + ".ClientOptions;\n";
76-
// Insert the import after the first import statement
77116
int firstImportEnd = contents.indexOf(";\n") + 2;
78117
contents = contents.substring(0, firstImportEnd)
79118
+ clientOptionsImport
80119
+ contents.substring(firstImportEnd);
81120
}
82121

122+
if (fileName.equals("UriPage") || fileName.equals("PathPage")) {
123+
String coreImports = "import " + corePackage + ".ClientOptions;\n"
124+
+ "import " + corePackage + ".ObjectMappers;\n"
125+
+ "import " + corePackage + ".RequestOptions;\n"
126+
+ "import " + corePackage + "." + apiExceptionSimpleName + ";\n"
127+
+ "import " + corePackage + "." + baseExceptionSimpleName + ";\n";
128+
int firstImportEnd = contents.indexOf(";\n") + 2;
129+
contents = contents.substring(0, firstImportEnd)
130+
+ coreImports
131+
+ contents.substring(firstImportEnd);
132+
contents = contents.replace("__API_EXCEPTION__", apiExceptionSimpleName);
133+
contents = contents.replace("__BASE_EXCEPTION__", baseExceptionSimpleName);
134+
}
135+
83136
// Apply custom pager name if configured
84137
String customPagerName = generatorContext.getCustomConfig() != null
85138
? generatorContext
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import java.io.IOException;
2+
import java.util.List;
3+
import java.util.Optional;
4+
import java.util.function.Function;
5+
import okhttp3.Headers;
6+
import okhttp3.OkHttpClient;
7+
import okhttp3.Request;
8+
import okhttp3.RequestBody;
9+
import okhttp3.Response;
10+
import okhttp3.ResponseBody;
11+
12+
/**
13+
* Utility for path-based pagination where the response contains a relative path for the next page.
14+
* The path is combined with the base URL from client options to form the full request URL.
15+
*/
16+
public final class PathPage {
17+
18+
private PathPage() {}
19+
20+
/**
21+
* Creates a {@link SyncPagingIterable} from an initial parsed response, using the provided
22+
* next path to fetch subsequent pages.
23+
*
24+
* @param <T> the item type
25+
* @param <R> the response type
26+
* @param initialResponse the parsed response from the initial request
27+
* @param nextPath the next page path extracted from the response (empty if no more pages)
28+
* @param items the items extracted from the initial response
29+
* @param responseClass the class of the response type for deserialization
30+
* @param getNext function to extract the next path from a parsed response
31+
* @param getItems function to extract the items list from a parsed response
32+
* @param httpMethod the HTTP method to use for follow-up requests (e.g., "GET", "POST")
33+
* @param requestBody the request body for follow-up requests (null for GET)
34+
* @param additionalHeaders extra headers to include (e.g., Accept, Content-Type)
35+
* @param clientOptions the client options for making HTTP requests
36+
* @param requestOptions the request options (headers, timeout, etc.)
37+
* @return a {@link SyncPagingIterable} that lazily fetches subsequent pages
38+
*/
39+
public static <T, R> SyncPagingIterable<T> create(
40+
R initialResponse,
41+
Optional<String> nextPath,
42+
List<T> items,
43+
Class<R> responseClass,
44+
Function<R, Optional<String>> getNext,
45+
Function<R, List<T>> getItems,
46+
String httpMethod,
47+
RequestBody requestBody,
48+
Headers additionalHeaders,
49+
ClientOptions clientOptions,
50+
RequestOptions requestOptions) {
51+
return new SyncPagingIterable<>(
52+
nextPath.isPresent(),
53+
items,
54+
initialResponse,
55+
nextPath.isPresent()
56+
? () -> fetchPage(
57+
nextPath.get(),
58+
responseClass,
59+
getNext,
60+
getItems,
61+
httpMethod,
62+
requestBody,
63+
additionalHeaders,
64+
clientOptions,
65+
requestOptions)
66+
: () -> null);
67+
}
68+
69+
private static <T, R> SyncPagingIterable<T> fetchPage(
70+
String path,
71+
Class<R> responseClass,
72+
Function<R, Optional<String>> getNext,
73+
Function<R, List<T>> getItems,
74+
String httpMethod,
75+
RequestBody requestBody,
76+
Headers additionalHeaders,
77+
ClientOptions clientOptions,
78+
RequestOptions requestOptions) {
79+
Request.Builder requestBuilder = new Request.Builder()
80+
.url(clientOptions.environment().getUrl() + path)
81+
.method(httpMethod, requestBody)
82+
.headers(Headers.of(clientOptions.headers(requestOptions)));
83+
for (int i = 0; i < additionalHeaders.size(); i++) {
84+
requestBuilder.addHeader(additionalHeaders.name(i), additionalHeaders.value(i));
85+
}
86+
Request okhttpRequest = requestBuilder.build();
87+
OkHttpClient client = clientOptions.httpClient();
88+
if (requestOptions != null && requestOptions.getTimeout().isPresent()) {
89+
client = clientOptions.httpClientWithTimeout(requestOptions);
90+
}
91+
try (Response response = client.newCall(okhttpRequest).execute()) {
92+
ResponseBody responseBody = response.body();
93+
String responseBodyString = responseBody != null ? responseBody.string() : "{}";
94+
if (response.isSuccessful()) {
95+
R parsedResponse = ObjectMappers.JSON_MAPPER.readValue(responseBodyString, responseClass);
96+
Optional<String> nextUrl = getNext.apply(parsedResponse);
97+
List<T> results = getItems.apply(parsedResponse);
98+
return new SyncPagingIterable<>(
99+
nextUrl.isPresent(),
100+
results,
101+
parsedResponse,
102+
nextUrl.isPresent()
103+
? () -> fetchPage(
104+
nextUrl.get(),
105+
responseClass,
106+
getNext,
107+
getItems,
108+
httpMethod,
109+
requestBody,
110+
additionalHeaders,
111+
clientOptions,
112+
requestOptions)
113+
: () -> null);
114+
}
115+
Object errorBody = ObjectMappers.parseErrorBody(responseBodyString);
116+
throw new __API_EXCEPTION__(
117+
"Error with status code " + response.code(), response.code(), errorBody, response);
118+
} catch (IOException e) {
119+
throw new __BASE_EXCEPTION__("Network error executing HTTP request", e);
120+
}
121+
}
122+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import java.io.IOException;
2+
import java.util.List;
3+
import java.util.Optional;
4+
import java.util.function.Function;
5+
import okhttp3.Headers;
6+
import okhttp3.OkHttpClient;
7+
import okhttp3.Request;
8+
import okhttp3.RequestBody;
9+
import okhttp3.Response;
10+
import okhttp3.ResponseBody;
11+
12+
/**
13+
* Utility for URI-based pagination where the response contains a full URL for the next page.
14+
*/
15+
public final class UriPage {
16+
17+
private UriPage() {}
18+
19+
/**
20+
* Creates a {@link SyncPagingIterable} from an initial parsed response, using the provided
21+
* next URI to fetch subsequent pages.
22+
*
23+
* @param <T> the item type
24+
* @param <R> the response type
25+
* @param initialResponse the parsed response from the initial request
26+
* @param nextUri the next page URI extracted from the response (empty if no more pages)
27+
* @param items the items extracted from the initial response
28+
* @param responseClass the class of the response type for deserialization
29+
* @param getNext function to extract the next URI from a parsed response
30+
* @param getItems function to extract the items list from a parsed response
31+
* @param httpMethod the HTTP method to use for follow-up requests (e.g., "GET", "POST")
32+
* @param requestBody the request body for follow-up requests (null for GET)
33+
* @param additionalHeaders extra headers to include (e.g., Accept, Content-Type)
34+
* @param clientOptions the client options for making HTTP requests
35+
* @param requestOptions the request options (headers, timeout, etc.)
36+
* @return a {@link SyncPagingIterable} that lazily fetches subsequent pages
37+
*/
38+
public static <T, R> SyncPagingIterable<T> create(
39+
R initialResponse,
40+
Optional<String> nextUri,
41+
List<T> items,
42+
Class<R> responseClass,
43+
Function<R, Optional<String>> getNext,
44+
Function<R, List<T>> getItems,
45+
String httpMethod,
46+
RequestBody requestBody,
47+
Headers additionalHeaders,
48+
ClientOptions clientOptions,
49+
RequestOptions requestOptions) {
50+
return new SyncPagingIterable<>(
51+
nextUri.isPresent(),
52+
items,
53+
initialResponse,
54+
nextUri.isPresent()
55+
? () -> fetchPage(
56+
nextUri.get(),
57+
responseClass,
58+
getNext,
59+
getItems,
60+
httpMethod,
61+
requestBody,
62+
additionalHeaders,
63+
clientOptions,
64+
requestOptions)
65+
: () -> null);
66+
}
67+
68+
private static <T, R> SyncPagingIterable<T> fetchPage(
69+
String url,
70+
Class<R> responseClass,
71+
Function<R, Optional<String>> getNext,
72+
Function<R, List<T>> getItems,
73+
String httpMethod,
74+
RequestBody requestBody,
75+
Headers additionalHeaders,
76+
ClientOptions clientOptions,
77+
RequestOptions requestOptions) {
78+
Request.Builder requestBuilder = new Request.Builder()
79+
.url(url)
80+
.method(httpMethod, requestBody)
81+
.headers(Headers.of(clientOptions.headers(requestOptions)));
82+
for (int i = 0; i < additionalHeaders.size(); i++) {
83+
requestBuilder.addHeader(additionalHeaders.name(i), additionalHeaders.value(i));
84+
}
85+
Request okhttpRequest = requestBuilder.build();
86+
OkHttpClient client = clientOptions.httpClient();
87+
if (requestOptions != null && requestOptions.getTimeout().isPresent()) {
88+
client = clientOptions.httpClientWithTimeout(requestOptions);
89+
}
90+
try (Response response = client.newCall(okhttpRequest).execute()) {
91+
ResponseBody responseBody = response.body();
92+
String responseBodyString = responseBody != null ? responseBody.string() : "{}";
93+
if (response.isSuccessful()) {
94+
R parsedResponse = ObjectMappers.JSON_MAPPER.readValue(responseBodyString, responseClass);
95+
Optional<String> nextUrl = getNext.apply(parsedResponse);
96+
List<T> results = getItems.apply(parsedResponse);
97+
return new SyncPagingIterable<>(
98+
nextUrl.isPresent(),
99+
results,
100+
parsedResponse,
101+
nextUrl.isPresent()
102+
? () -> fetchPage(
103+
nextUrl.get(),
104+
responseClass,
105+
getNext,
106+
getItems,
107+
httpMethod,
108+
requestBody,
109+
additionalHeaders,
110+
clientOptions,
111+
requestOptions)
112+
: () -> null);
113+
}
114+
Object errorBody = ObjectMappers.parseErrorBody(responseBodyString);
115+
throw new __API_EXCEPTION__(
116+
"Error with status code " + response.code(), response.code(), errorBody, response);
117+
} catch (IOException e) {
118+
throw new __BASE_EXCEPTION__("Network error executing HTTP request", e);
119+
}
120+
}
121+
}

generators/java/sdk/src/main/java/com/fern/java/client/Cli.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,20 @@ public GeneratedRootClient generateClient(
294294
GeneratedJavaFile generatedRequestOptions = requestOptionsGenerator.generateFile();
295295
this.addGeneratedFile(generatedRequestOptions);
296296

297-
PaginationCoreGenerator paginationCoreGenerator = new PaginationCoreGenerator(context, generatorExecClient);
297+
String apiExceptionSimpleName = context.getPoetClassNameFactory()
298+
.getApiErrorClassName(
299+
context.getGeneratorConfig().getOrganization(),
300+
context.getGeneratorConfig().getWorkspaceName(),
301+
context.getCustomConfig())
302+
.simpleName();
303+
String baseExceptionSimpleName = context.getPoetClassNameFactory()
304+
.getBaseExceptionClassName(
305+
context.getGeneratorConfig().getOrganization(),
306+
context.getGeneratorConfig().getWorkspaceName(),
307+
context.getCustomConfig())
308+
.simpleName();
309+
PaginationCoreGenerator paginationCoreGenerator =
310+
new PaginationCoreGenerator(context, generatorExecClient, apiExceptionSimpleName, baseExceptionSimpleName);
298311
List<GeneratedFile> generatedFiles = paginationCoreGenerator.generateFiles();
299312
generatedFiles.forEach(this::addGeneratedFile);
300313

0 commit comments

Comments
 (0)