TypeSpec recently added support for pagination that allows services to define APIs that return a paginated collection of results.
There are two types of pagination:
- Client driven pagination
- Server driven pagination
A service can choose to support either or both of these pagination types.
This type allows the client to control the pagination behavior. The client maintains the necessary state to request the next set of results from the server. TypeSpec supports configuring the following in a client request:
- Page Size - number of items in a page
- Page Index - the page number
- Offset - number of items to skip
The server can return additional metadata along with the results to inform the client how to fetch the next set of results. The metadata can contain a continuation token or a set of links that can let the client fetch first, last, next or previous page.
Option 1: Create a common paging type in core
This approach is similar to what we have in Azure. The base paging types will be defined in core with generics that allow library operations to define the generic types.
Let's use an example that supports all above scenarios
@list op listPets(@query @pageIndex page?: int32, @query @pageSize perPage?: int32): {
@pageItems pets: Pet[];
@nextLink next?: url;
@prevLink prev?: url;
@firstLink first?: url;
@lastLink last?: url;
};
@list op listPetStores(String zipCode, String continuationToken): {
@pageItems petStores: PetStore[];
@continuationToken continuationToken: string;
};
public class PagedResponse<T> implements Response<List<T>> {
public List<T> getItems() {}
public String getContinuationToken() {}
public String getNextLink() {}
public String getPreviousLink() {}
public String getFirstLink() {}
public String getLastLink() {}
}
public class PagedIterable<T> {
public Iterable<T> iterable() {}
public Iterable<PagedResponse<T>> iterableByPage() {}
public Iterable<PagedResponse<T>> iterableByPage(PagingOptions options) {}
public Stream<T> stream() {}
public Stream<PagedResponse<T>> streamByPage() {}
public Stream<PagedResponse<T>> streamByPage(PagingOptions options) {}
}
public class PagingOptions {
private Long offset;
private Long pageSize;
private Long pageIndex;
// TypeSpec only support string continuation tokens
private String continuationToken;
// public getters and setters
}
public class PetClient {
public PagedIterable<Pet> listPets();
public PagedIterable<PetStore> listPetStores(String zipCode);
}
PetClient client = instantiateClient();
// Listing pets
PagedIterable<Pet> petIterable = client.listPets();
// to iterate through all pets
petIterable.stream()
.forEach(pet -> System.out.println(pet.getName()));
// to iterate through pages
petIterable.streamByPage()
.flatMap(pagedResponse -> pagedResponse.getItems().stream())
.forEach(pet -> System.out.println(pet.getName()));
// to iterate through pages starting from some offset
PagingOptions petPagingOptions = new PagingOptions()
.setPageIndex(5)
.setPageSize(10)
.setOffset(50);
petIterable.streamByPage(petPagingOptions)
.peek(pagedResponse -> System.out.println(pagedResponse.getNextLink()))
.flatMap(pagedResponse -> pagedResponse.getItems().stream())
.forEach(pet -> System.out.println(pet.getName()));
// Listing pet stores
PagedIterable<PetStore> petStoreIterable = client.listPetStores("11235");
// to iterate through pages from a continuation token
PagingOptions petPagingOptions = new PagingOptions()
.setOffset(50) // this will not work as the operation doesn't support it
.setContinuationToken("xuw1dv");
petStoreIterable.streamByPage(petStorePagingOptions)
.peek(pagedResponse -> System.out.println(pagedResponse.getContinuationToken()))
.peek(pagedResponse -> System.out.println(pagedResponse.getNextLink())) // this will always be null as the op doesn't return these links
.flatMap(pagedResponse -> pagedResponse.getItems().stream())
.forEach(petStore -> System.out.println(petStore.getAddress()));
Pros
- Consistent paging experience across libraries
Cons
- The common paging type will have to support all of the above scenarios while only a subset of the scenarios are supported by the service
Option 2: Create a paging type per library
This approach will create a paging type per library and will tailor the APIs to support only the paging scenarios the service supports.
@list op listPets(@query @pageSize perPage?: int32): {
@pageItems pets: Pet[];
@nextLink next?: url;
};
@list op listPetStores(String zipCode, String continuationToken): {
@pageItems petStores: PetStore[];
@continuationToken continuationToken: string;
};
public class PetServicePagedResponse<T> implements Response<List<T>> {
public List<T> getItems() {}
public String getContinuationToken() {}
public String getNextLink();
}
public class PetServicePagingOptions {
private Long pageSize;
// TypeSpec only support string continuation tokens
private String continuationToken;
// public getters and setters
}
public class PetServicePagedIterable<T> {
public Iterable<T> iterable() {}
public Iterable<PetServicePagedResponse<T>> iterableByPage() {}
public Iterable<PetServicePagedResponse<T>> iterableByPage(PetServicePagingOptions options) {}
public Stream<T> stream() {}
public Stream<PetServicePagedResponse<T>> streamByPage() {}
public Stream<PetServicePagedResponse<T>> streamByPage(PetServicePagingOptions options) {}
}
public class PetClient {
public PetServicePagedIterable<Pet> listPets();
public PetServicePagedIterable<PetStore> listPetStores(String zipCode);
}
PetClient client = instantiateClient();
// Listing pets
PetServicePagedIterable<Pet> petIterable = client.listPets();
// to iterate through all pets
petIterable.stream()
.forEach(pet -> System.out.println(pet.getName()));
// to iterate through pages
petIterable.streamByPage()
.peek(pagedResponse -> System.out.println(pagedResponse.getContinuationToken()) // this will always be null
.peek(pagedResponse -> System.out.println(pagedResponse.getNextLink()))
.flatMap(pagedResponse -> pagedResponse.getItems().stream())
.forEach(pet -> System.out.println(pet.getName()));
// to iterate through pages starting from some offset
PagingOptions petPagingOptions = new PagingOptions()
.setPageIndex(5)
.setPageSize(10)
.setOffset(50);
petIterable.streamByPage(petPagingOptions)
.peek(pagedResponse -> System.out.println(pagedResponse.getNextLink()))
.flatMap(pagedResponse -> pagedResponse.getItems().stream())
.forEach(pet -> System.out.println(pet.getName()));
// Listing pet stores
PetServicePagedIterable<PetStore> petStoreIterable = client.listPetStores("11235");
// to iterate through pages from a continuation token
petStoreIterable.streamByPage("xuw1dv")
.peek(pagedResponse -> System.out.println(pagedResponse.getContinuationToken()))
.peek(pagedResponse -> System.out.println(pagedResponse.getNextLink())) // will always be null
.flatMap(pagedResponse -> pagedResponse.getItems().stream())
.forEach(petStore -> System.out.println(petStore.getAddress()));
Pros
- Provides paging APIs that the service supports
- Allows adding any additional information in the PageResponse type that is specific to the service
Cons
- Requires a new paging type to be created for every library that supports pagination. Also, the paging type defined at the library will support all scenarios the service supports that may or may not be applicable to a specific operation. A service is generally expected to have same paging scenarios supported for all operations but there could be reasons for different operations to have different capabilities.
Option 3: Create a paging type per operation
This is the most granular level of supporting pagination. Every operation defines its own paging type and will contain APIs specific to that operation.
@list op listPets(@query @pageIndex page?: int32, @query @pageSize perPage?: int32): {
@pageItems pets: Pet[];
@nextLink next?: url;
@prevLink prev?: url;
@firstLink first?: url;
@lastLink last?: url;
};
@list op listPetStores(String zipCode, String continuationToken): {
@pageItems petStores: PetStore[];
@continuationToken continuationToken: string;
};
public class PetPagedResponse implements Response<List<Pet>> {
public List<Pet> getPets() {}
public String getNextLink() {}
public String getPreviousLink() {}
public String getFirstLink() {}
public String getLastLink() {}
}
public class PetsPagingOptions {
private Long offset;
private Long pageSize;
private Long pageIndex;
// public getters and setters
}
public class PetsIterable {
public Iterable<Pet> iterable() {}
public Iterable<PetPagedResponse> iterableByPage() {}
public Iterable<PetPagedResponse> iterableByPage(PetsPagingOptions options) {}
public Stream<Pet> stream() {}
public Stream<PetPagedResponse> streamByPage() {}
public Stream<PetPagedResponse> streamByPage(PetsPagingOptions options) {}
}
public class PetStorePagedResponse implements Response<List<PetStore>> {
public List<PetStore> getPetStores() {}
public String getContinuationToken() {}
}
public class PetStoresIterable {
public Iterable<PetStores> iterable() {}
public Iterable<PetStoresPagedResponse> iterableByPage() {}
public Iterable<PetStoresPagedResponse> iterableByPage(String continuationToken) {}
public Stream<PetStores> stream() {}
public Stream<PetStoresPagedResponse> streamByPage() {}
public Stream<PetStoresPagedResponse> streamByPage(String continuationToken) {}
}
public class PetClient {
public PetsIterable listPets();
public PetStoresIterable listPetStores(String zipCode);
}
PetClient client = instantiateClient();
// Listing pets
PetsIterable petIterable = client.listPets();
// to iterate through all pets
petIterable.stream()
.forEach(pet -> System.out.println(pet.getName()));
// to iterate through pages
petIterable.streamByPage()
.flatMap(pagedResponse -> pagedResponse.getItems().stream())
.forEach(pet -> System.out.println(pet.getName()));
// to iterate through pages starting from some offset
PagingOptions petPagingOptions = new PagingOptions()
.setPageIndex(5)
.setPageSize(10)
.setOffset(50);
petIterable.streamByPage(petPagingOptions)
.flatMap(pagedResponse -> pagedResponse.getItems().stream())
.forEach(pet -> System.out.println(pet.getName()));
// Listing pet stores
PetStoresIterable petStoreIterable = client.listPetStores("11235");
// to iterate through pages from a continuation token
petStoreIterable.streamByPage("xuw1dv")
.peek(pagedResponse -> System.out.println(pagedResponse.getContinuationToken()))
.flatMap(pagedResponse -> pagedResponse.getItems().stream())
.forEach(petStore -> System.out.println(petStore.getAddress()));
Pros
- Very granular and provides users with APIs that are supported by that particular operation.
- Allows adding any additional information in the PageResponse type that is specific to the operation
Cons
- Proliferation of paging types for each library and each paging operation.
I seem to prefer option 2. As it offers a hybrid approach with common base types for extensibility is possible. Option 2 also helps with keeping up with particular service needs and still providing consistency to users. Also, usage wise option 2 is much similar to option 1 with lesser overwhelming APIs (only supported APIs by the service) as opposed to option 1.
For option 3, becomes too granular, might make it confusing for users with too many variations.