Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion generators/java/generator-utils/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
dependencies {
api 'com.squareup:javapoet'
api 'com.fern.fern:irV63'
api 'com.fern.fern:irV64'
api 'com.fern.fern:generator-exec-client'
api 'com.fasterxml.jackson.core:jackson-annotations'
api 'com.fasterxml.jackson.core:jackson-databind'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,35 @@

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

import com.fern.ir.model.http.HttpEndpoint;
import com.fern.ir.model.http.HttpService;
import com.fern.ir.model.http.Pagination;
import com.fern.java.AbstractGeneratorContext;
import com.fern.java.DefaultGeneratorExecClient;
import com.fern.java.output.GeneratedFile;
import com.fern.java.output.GeneratedResourcesJavaFile;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public final class PaginationCoreGenerator extends AbstractFilesGenerator {
public static final String GET_MODULE_METHOD_NAME = "getModule";
private final DefaultGeneratorExecClient generatorExecClient;
private final String apiExceptionSimpleName;
private final String baseExceptionSimpleName;

public PaginationCoreGenerator(
AbstractGeneratorContext<?, ?> generatorContext, DefaultGeneratorExecClient generatorExecClient) {
AbstractGeneratorContext<?, ?> generatorContext,
DefaultGeneratorExecClient generatorExecClient,
String apiExceptionSimpleName,
String baseExceptionSimpleName) {
super(generatorContext);
this.generatorExecClient = generatorExecClient;
this.apiExceptionSimpleName = apiExceptionSimpleName;
this.baseExceptionSimpleName = baseExceptionSimpleName;
}

@Override
Expand All @@ -56,8 +67,37 @@ public List<GeneratedFile> generateFiles() {
return List.of();
}

List<String> fileNames = List.of(
"BasePage", "SyncPage", "SyncPagingIterable", "BiDirectionalPage", "CustomPager", "AsyncCustomPager");
// Scan IR to determine which pagination types are in use
boolean hasCustomPagination = false;
boolean hasUriPagination = false;
boolean hasPathPagination = false;
for (HttpService service : generatorContext.getIr().getServices().values()) {
for (HttpEndpoint endpoint : service.getEndpoints()) {
if (endpoint.getPagination().isPresent()) {
Pagination pagination = endpoint.getPagination().get();
if (pagination.isCustom()) {
hasCustomPagination = true;
} else if (pagination.isUri()) {
hasUriPagination = true;
} else if (pagination.isPath()) {
hasPathPagination = true;
}
}
}
}

List<String> fileNames = new ArrayList<>(List.of("BasePage", "SyncPage", "SyncPagingIterable"));
if (hasCustomPagination) {
fileNames.add("BiDirectionalPage");
fileNames.add("CustomPager");
fileNames.add("AsyncCustomPager");
}
if (hasUriPagination) {
fileNames.add("UriPage");
}
if (hasPathPagination) {
fileNames.add("PathPage");
}

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

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

// Add ClientOptions import for CustomPager and AsyncCustomPager
// Replace core package placeholders in templates
if (fileName.equals("CustomPager") || fileName.equals("AsyncCustomPager")) {
String clientOptionsImport = "import " + corePackage + ".ClientOptions;\n";
// Insert the import after the first import statement
int firstImportEnd = contents.indexOf(";\n") + 2;
contents = contents.substring(0, firstImportEnd)
+ clientOptionsImport
+ contents.substring(firstImportEnd);
}

if (fileName.equals("UriPage") || fileName.equals("PathPage")) {
String coreImports = "import " + corePackage + ".ClientOptions;\n"
+ "import " + corePackage + ".ObjectMappers;\n"
+ "import " + corePackage + ".RequestOptions;\n"
+ "import " + corePackage + "." + apiExceptionSimpleName + ";\n"
+ "import " + corePackage + "." + baseExceptionSimpleName + ";\n";
int firstImportEnd = contents.indexOf(";\n") + 2;
contents = contents.substring(0, firstImportEnd)
+ coreImports
+ contents.substring(firstImportEnd);
contents = contents.replace("__API_EXCEPTION__", apiExceptionSimpleName);
contents = contents.replace("__BASE_EXCEPTION__", baseExceptionSimpleName);
}

// Apply custom pager name if configured
String customPagerName = generatorContext.getCustomConfig() != null
? generatorContext
Expand Down
122 changes: 122 additions & 0 deletions generators/java/generator-utils/src/main/resources/PathPage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

/**
* Utility for path-based pagination where the response contains a relative path for the next page.
* The path is combined with the base URL from client options to form the full request URL.
*/
public final class PathPage {

private PathPage() {}

/**
* Creates a {@link SyncPagingIterable} from an initial parsed response, using the provided
* next path to fetch subsequent pages.
*
* @param <T> the item type
* @param <R> the response type
* @param initialResponse the parsed response from the initial request
* @param nextPath the next page path extracted from the response (empty if no more pages)
* @param items the items extracted from the initial response
* @param responseClass the class of the response type for deserialization
* @param getNext function to extract the next path from a parsed response
* @param getItems function to extract the items list from a parsed response
* @param httpMethod the HTTP method to use for follow-up requests (e.g., "GET", "POST")
* @param requestBody the request body for follow-up requests (null for GET)
* @param additionalHeaders extra headers to include (e.g., Accept, Content-Type)
* @param clientOptions the client options for making HTTP requests
* @param requestOptions the request options (headers, timeout, etc.)
* @return a {@link SyncPagingIterable} that lazily fetches subsequent pages
*/
public static <T, R> SyncPagingIterable<T> create(
R initialResponse,
Optional<String> nextPath,
List<T> items,
Class<R> responseClass,
Function<R, Optional<String>> getNext,
Function<R, List<T>> getItems,
String httpMethod,
RequestBody requestBody,
Headers additionalHeaders,
ClientOptions clientOptions,
RequestOptions requestOptions) {
return new SyncPagingIterable<>(
nextPath.isPresent(),
items,
initialResponse,
nextPath.isPresent()
? () -> fetchPage(
nextPath.get(),
responseClass,
getNext,
getItems,
httpMethod,
requestBody,
additionalHeaders,
clientOptions,
requestOptions)
: () -> null);
}

private static <T, R> SyncPagingIterable<T> fetchPage(
String path,
Class<R> responseClass,
Function<R, Optional<String>> getNext,
Function<R, List<T>> getItems,
String httpMethod,
RequestBody requestBody,
Headers additionalHeaders,
ClientOptions clientOptions,
RequestOptions requestOptions) {
Request.Builder requestBuilder = new Request.Builder()
.url(clientOptions.environment().getUrl() + path)
.method(httpMethod, requestBody)
.headers(Headers.of(clientOptions.headers(requestOptions)));
for (int i = 0; i < additionalHeaders.size(); i++) {
requestBuilder.addHeader(additionalHeaders.name(i), additionalHeaders.value(i));
}
Request okhttpRequest = requestBuilder.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()) {
R parsedResponse = ObjectMappers.JSON_MAPPER.readValue(responseBodyString, responseClass);
Optional<String> nextUrl = getNext.apply(parsedResponse);
List<T> results = getItems.apply(parsedResponse);
return new SyncPagingIterable<>(
nextUrl.isPresent(),
results,
parsedResponse,
nextUrl.isPresent()
? () -> fetchPage(
nextUrl.get(),
responseClass,
getNext,
getItems,
httpMethod,
requestBody,
additionalHeaders,
clientOptions,
requestOptions)
: () -> null);
}
Object errorBody = ObjectMappers.parseErrorBody(responseBodyString);
throw new __API_EXCEPTION__(
"Error with status code " + response.code(), response.code(), errorBody, response);
} catch (IOException e) {
throw new __BASE_EXCEPTION__("Network error executing HTTP request", e);
}
}
}
121 changes: 121 additions & 0 deletions generators/java/generator-utils/src/main/resources/UriPage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

/**
* Utility for URI-based pagination where the response contains a full URL for the next page.
*/
public final class UriPage {

private UriPage() {}

/**
* Creates a {@link SyncPagingIterable} from an initial parsed response, using the provided
* next URI to fetch subsequent pages.
*
* @param <T> the item type
* @param <R> the response type
* @param initialResponse the parsed response from the initial request
* @param nextUri the next page URI extracted from the response (empty if no more pages)
* @param items the items extracted from the initial response
* @param responseClass the class of the response type for deserialization
* @param getNext function to extract the next URI from a parsed response
* @param getItems function to extract the items list from a parsed response
* @param httpMethod the HTTP method to use for follow-up requests (e.g., "GET", "POST")
* @param requestBody the request body for follow-up requests (null for GET)
* @param additionalHeaders extra headers to include (e.g., Accept, Content-Type)
* @param clientOptions the client options for making HTTP requests
* @param requestOptions the request options (headers, timeout, etc.)
* @return a {@link SyncPagingIterable} that lazily fetches subsequent pages
*/
public static <T, R> SyncPagingIterable<T> create(
R initialResponse,
Optional<String> nextUri,
List<T> items,
Class<R> responseClass,
Function<R, Optional<String>> getNext,
Function<R, List<T>> getItems,
String httpMethod,
RequestBody requestBody,
Headers additionalHeaders,
ClientOptions clientOptions,
RequestOptions requestOptions) {
return new SyncPagingIterable<>(
nextUri.isPresent(),
items,
initialResponse,
nextUri.isPresent()
? () -> fetchPage(
nextUri.get(),
responseClass,
getNext,
getItems,
httpMethod,
requestBody,
additionalHeaders,
clientOptions,
requestOptions)
: () -> null);
}

private static <T, R> SyncPagingIterable<T> fetchPage(
String url,
Class<R> responseClass,
Function<R, Optional<String>> getNext,
Function<R, List<T>> getItems,
String httpMethod,
RequestBody requestBody,
Headers additionalHeaders,
ClientOptions clientOptions,
RequestOptions requestOptions) {
Request.Builder requestBuilder = new Request.Builder()
.url(url)
.method(httpMethod, requestBody)
.headers(Headers.of(clientOptions.headers(requestOptions)));
for (int i = 0; i < additionalHeaders.size(); i++) {
requestBuilder.addHeader(additionalHeaders.name(i), additionalHeaders.value(i));
}
Request okhttpRequest = requestBuilder.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()) {
R parsedResponse = ObjectMappers.JSON_MAPPER.readValue(responseBodyString, responseClass);
Optional<String> nextUrl = getNext.apply(parsedResponse);
List<T> results = getItems.apply(parsedResponse);
return new SyncPagingIterable<>(
nextUrl.isPresent(),
results,
parsedResponse,
nextUrl.isPresent()
? () -> fetchPage(
nextUrl.get(),
responseClass,
getNext,
getItems,
httpMethod,
requestBody,
additionalHeaders,
clientOptions,
requestOptions)
: () -> null);
}
Object errorBody = ObjectMappers.parseErrorBody(responseBodyString);
throw new __API_EXCEPTION__(
"Error with status code " + response.code(), response.code(), errorBody, response);
} catch (IOException e) {
throw new __BASE_EXCEPTION__("Network error executing HTTP request", e);
}
}
}
15 changes: 14 additions & 1 deletion generators/java/sdk/src/main/java/com/fern/java/client/Cli.java
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,20 @@ public GeneratedRootClient generateClient(
GeneratedJavaFile generatedRequestOptions = requestOptionsGenerator.generateFile();
this.addGeneratedFile(generatedRequestOptions);

PaginationCoreGenerator paginationCoreGenerator = new PaginationCoreGenerator(context, generatorExecClient);
String apiExceptionSimpleName = context.getPoetClassNameFactory()
.getApiErrorClassName(
context.getGeneratorConfig().getOrganization(),
context.getGeneratorConfig().getWorkspaceName(),
context.getCustomConfig())
.simpleName();
String baseExceptionSimpleName = context.getPoetClassNameFactory()
.getBaseExceptionClassName(
context.getGeneratorConfig().getOrganization(),
context.getGeneratorConfig().getWorkspaceName(),
context.getCustomConfig())
.simpleName();
PaginationCoreGenerator paginationCoreGenerator =
new PaginationCoreGenerator(context, generatorExecClient, apiExceptionSimpleName, baseExceptionSimpleName);
List<GeneratedFile> generatedFiles = paginationCoreGenerator.generateFiles();
generatedFiles.forEach(this::addGeneratedFile);

Expand Down
Loading
Loading