consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace StellaOps.TaskRunner.Client.Pagination;
|
||||
|
||||
/// <summary>
|
||||
/// Generic paginator for API responses.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of items being paginated.</typeparam>
|
||||
public sealed class Paginator<T>
|
||||
{
|
||||
private readonly Func<int, int, CancellationToken, Task<PagedResponse<T>>> _fetchPage;
|
||||
private readonly int _pageSize;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new paginator.
|
||||
/// </summary>
|
||||
/// <param name="fetchPage">Function to fetch a page (offset, limit, cancellationToken) -> page.</param>
|
||||
/// <param name="pageSize">Number of items per page (default: 50).</param>
|
||||
public Paginator(
|
||||
Func<int, int, CancellationToken, Task<PagedResponse<T>>> fetchPage,
|
||||
int pageSize = 50)
|
||||
{
|
||||
_fetchPage = fetchPage ?? throw new ArgumentNullException(nameof(fetchPage));
|
||||
_pageSize = pageSize > 0 ? pageSize : throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates through all pages asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Async enumerable of items.</returns>
|
||||
public async IAsyncEnumerable<T> GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var offset = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var page = await _fetchPage(offset, _pageSize, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var item in page.Items)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
|
||||
if (!page.HasMore || page.Items.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
offset += page.Items.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects all items into a list.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>List of all items.</returns>
|
||||
public async Task<IReadOnlyList<T>> CollectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = new List<T>();
|
||||
|
||||
await foreach (var item in GetAllAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single page.
|
||||
/// </summary>
|
||||
/// <param name="pageNumber">Page number (1-based).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Single page response.</returns>
|
||||
public Task<PagedResponse<T>> GetPageAsync(int pageNumber, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (pageNumber < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(pageNumber), "Page number must be >= 1.");
|
||||
}
|
||||
|
||||
var offset = (pageNumber - 1) * _pageSize;
|
||||
return _fetchPage(offset, _pageSize, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paginated response wrapper.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of items.</typeparam>
|
||||
public sealed record PagedResponse<T>(
|
||||
IReadOnlyList<T> Items,
|
||||
int TotalCount,
|
||||
bool HasMore)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an empty page.
|
||||
/// </summary>
|
||||
public static PagedResponse<T> Empty { get; } = new([], 0, false);
|
||||
|
||||
/// <summary>
|
||||
/// Current page number (1-based) based on offset and page size.
|
||||
/// </summary>
|
||||
public int PageNumber(int offset, int pageSize)
|
||||
=> pageSize > 0 ? (offset / pageSize) + 1 : 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for creating paginators.
|
||||
/// </summary>
|
||||
public static class PaginatorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a paginator from a fetch function.
|
||||
/// </summary>
|
||||
public static Paginator<T> Paginate<T>(
|
||||
this Func<int, int, CancellationToken, Task<PagedResponse<T>>> fetchPage,
|
||||
int pageSize = 50)
|
||||
=> new(fetchPage, pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// Takes the first N items from an async enumerable.
|
||||
/// </summary>
|
||||
public static async IAsyncEnumerable<T> TakeAsync<T>(
|
||||
this IAsyncEnumerable<T> source,
|
||||
int count,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
if (count <= 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var taken = 0;
|
||||
await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return item;
|
||||
taken++;
|
||||
if (taken >= count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips the first N items from an async enumerable.
|
||||
/// </summary>
|
||||
public static async IAsyncEnumerable<T> SkipAsync<T>(
|
||||
this IAsyncEnumerable<T> source,
|
||||
int count,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
var skipped = 0;
|
||||
await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (skipped < count)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user