Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(csharp): Add custom pager #6242

Merged
merged 23 commits into from
Mar 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ddc8b2f
Refactor RawClient.cs
Swimburger Feb 27, 2025
e6a6b48
Add custom pager without endpoint implementation (WIP)
Swimburger Feb 27, 2025
b0024cc
Add custom pagination to OpenAPI/FD/IR
Swimburger Feb 27, 2025
9943233
refactor pagination to have type property
Swimburger Feb 28, 2025
7161333
clean up imports
Swimburger Feb 28, 2025
219fb57
Merge branch 'main' of https://github.com/fern-api/fern into niels/cs…
Swimburger Feb 28, 2025
8d3d593
WIP custom pager
Swimburger Feb 28, 2025
de0bf0d
Merge branch 'main' of https://github.com/fern-api/fern into niels/cs…
Swimburger Feb 28, 2025
8d06bff
merge
Swimburger Feb 28, 2025
b69383d
chore: update changelog
fern-support Feb 28, 2025
6280165
Fix imports
Swimburger Feb 28, 2025
f9ed04d
fix custom pager
Swimburger Feb 28, 2025
60528f4
Merge branch 'niels/csharp/custom-pager' of https://github.com/fern-a…
Swimburger Feb 28, 2025
3ca2973
Fix default custom pager name
Swimburger Feb 28, 2025
5d8dcfd
update rawclient and rawclient test
Swimburger Mar 1, 2025
b0519ba
Update versions.yml
Swimburger Mar 1, 2025
21f4565
chore: update changelog
fern-support Mar 1, 2025
4a882ed
remove redundant code
Swimburger Mar 1, 2025
2676987
Merge branch 'niels/csharp/custom-pager' of https://github.com/fern-a…
Swimburger Mar 1, 2025
3e4f525
test update
Swimburger Mar 1, 2025
dba209e
regen jsonschema
Swimburger Mar 1, 2025
48da449
Add shouldGenerate to FileGenerator, update rawclient, don't generate…
Swimburger Mar 1, 2025
f5ceecd
reseed
Swimburger Mar 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions fern/pages/changelogs/csharp-sdk/2025-02-28.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.12.0-rc0
**`(feat):`** Add support for custom pagination.

2 changes: 1 addition & 1 deletion generators/csharp/codegen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@fern-api/fs-utils": "workspace:*",
"@fern-api/base-generator": "workspace:*",
"@fern-api/logging-execa": "workspace:*",
"@fern-fern/ir-sdk": "^55.4.0",
"@fern-fern/ir-sdk": "^56.0.0",
"lodash-es": "^4.17.21",
"zod": "^3.22.3"
},
Expand Down
1 change: 1 addition & 0 deletions generators/csharp/codegen/src/AsIs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const AsIsFiles = {
HttpMethodExtensions: "HttpMethodExtensions.cs",
Page: "Page.Template.cs",
Pager: "Pager.Template.cs",
CustomPager: "CustomPager.Template.cs",
ProtoAnyMapper: "ProtoAnyMapper.Template.cs",
RawClient: "RawClient.Template.cs",
RawGrpcClient: "RawGrpcClient.Template.cs",
Expand Down
12 changes: 11 additions & 1 deletion generators/csharp/codegen/src/FileGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,20 @@ export abstract class FileGenerator<
constructor(protected readonly context: Context) {}

public generate(): GeneratedFile {
this.context.logger.debug(`Generating ${this.getFilepath()}`);
if (this.shouldGenerate()) {
this.context.logger.debug(`Generating ${this.getFilepath()}`);
} else {
this.context.logger.warn(
`Internal warning: Generating ${this.getFilepath()} even though the file generator should not have been called.`
);
}
return this.doGenerate();
}

public shouldGenerate(): boolean {
return true;
}

protected abstract doGenerate(): GeneratedFile;

protected abstract getFilepath(): RelativeFilePath;
Expand Down
153 changes: 153 additions & 0 deletions generators/csharp/codegen/src/asIs/CustomPager.Template.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using System.Runtime.CompilerServices;
using global::System.Net.Http;

namespace <%= namespace%>;

internal static class CustomPagerFactory
{
public static async Task<CustomPager<TItem>> CreateAsync<TItem>(
Func<HttpRequestMessage, CancellationToken,
Task<HttpResponseMessage>> sendRequest,
HttpRequestMessage initialRequest,
CancellationToken cancellationToken = default
)
{
var response = await sendRequest(initialRequest, cancellationToken).ConfigureAwait(false);
var (
nextPageRequest,
hasNextPage,
previousPageRequest,
hasPreviousPage,
page
) = await CustomPager<TItem>.ParseHttpCallAsync(initialRequest, response, cancellationToken)
.ConfigureAwait(false);
return new CustomPager<TItem>(
sendRequest,
nextPageRequest,
hasNextPage,
previousPageRequest,
hasPreviousPage,
page
);
}
}

public class CustomPager<TItem> : BiPager<TItem>, IAsyncEnumerable<TItem>
{
private HttpRequestMessage? _nextPageRequest;
private HttpRequestMessage? _previousPageRequest;

private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendRequest;

public bool HasNextPage { get; private set; }
public bool HasPreviousPage { get; private set; }
public Page<TItem> CurrentPage { get; private set; }

public CustomPager(
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendRequest,
HttpRequestMessage? nextPageRequest,
bool hasNextPage,
HttpRequestMessage? previousPageRequest,
bool hasPreviousPage,
Page<TItem> page
)
{
_sendRequest = sendRequest;
_nextPageRequest = nextPageRequest;
HasNextPage = hasNextPage;
_previousPageRequest = previousPageRequest;
HasPreviousPage = hasPreviousPage;
CurrentPage = page;
}

public async Task<Page<TItem>> GetNextPageAsync(CancellationToken cancellationToken = default)
{
if (_nextPageRequest == null)
{
return Page<TItem>.Empty;
}

return await SendRequestAndHandleResponse(_nextPageRequest, cancellationToken)
.ConfigureAwait(false);
}

public async Task<Page<TItem>> GetPreviousPageAsync(CancellationToken cancellationToken = default)
{
if (_previousPageRequest == null)
{
return Page<TItem>.Empty;
}

return await SendRequestAndHandleResponse(_previousPageRequest, cancellationToken)
.ConfigureAwait(false);
}

private async Task<Page<TItem>> SendRequestAndHandleResponse(
HttpRequestMessage request,
CancellationToken cancellationToken = default)
{
var response = await _sendRequest(request, cancellationToken).ConfigureAwait(false);
var (
nextPageRequest,
hasNextPage,
previousPageRequest,
hasPreviousPage,
page
) = await ParseHttpCallAsync(request, response, cancellationToken).ConfigureAwait(false);
_nextPageRequest = nextPageRequest;
HasNextPage = hasNextPage;
_previousPageRequest = previousPageRequest;
HasPreviousPage = hasPreviousPage;
CurrentPage = page;
return page;
}

internal static async Task<(
HttpRequestMessage? nextPageRequest,
bool hasNextPage,
HttpRequestMessage? previousPageRequest,
bool hasPreviousPage,
Page<TItem> page
)> ParseHttpCallAsync(
HttpRequestMessage request,
HttpResponseMessage response,
CancellationToken cancellationToken = default
)
{
throw new NotImplementedException();
}

public async IAsyncEnumerator<TItem> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
foreach (var item in CurrentPage)
{
yield return item;
}

await foreach (var page in GetNextPagesAsync(cancellationToken))
{
foreach (var item in page)
{
yield return item;
}
}
}

public async IAsyncEnumerable<Page<TItem>> GetNextPagesAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
while (HasNextPage)
{
yield return await GetNextPageAsync(cancellationToken).ConfigureAwait(false);
}
}

public async IAsyncEnumerable<Page<TItem>> GetPreviousPagesAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
while (HasPreviousPage)
{
yield return await GetPreviousPageAsync(cancellationToken).ConfigureAwait(false);
}
}
}
23 changes: 23 additions & 0 deletions generators/csharp/codegen/src/asIs/Page.Template.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections;
using System.Collections.ObjectModel;

namespace <%= namespace%>;

Expand All @@ -9,6 +10,10 @@ namespace <%= namespace%>;
/// <typeparam name="TItem">The type of items.</typeparam>
public class Page<TItem> : IEnumerable<TItem>
{
/// <summary>
/// Creates a new <see cref="Page{TItem}"/> with the specified items.
/// </summary>
/// <param name="items"></param>
public Page(IReadOnlyList<TItem> items)
{
Items = items;
Expand All @@ -19,7 +24,25 @@ public Page(IReadOnlyList<TItem> items)
/// </summary>
public IReadOnlyList<TItem> Items { get; }

/// <summary>
/// Enumerate the items in this <see cref="Page{TItem}"/>.
/// </summary>
/// <returns></returns>
public IEnumerator<TItem> GetEnumerator() => Items.GetEnumerator();

/// <summary>
/// Enumerate the items in this <see cref="Page{TItem}"/>.
/// </summary>
/// <returns></returns>
IEnumerator IEnumerable.GetEnumerator() => Items.GetEnumerator();

/// <summary>
/// An empty <see cref="Page{TItem}"/>.
/// </summary>
public static Page<TItem> Empty { get; } = new(new ReadOnlyCollection<TItem>(Array.Empty<TItem>()));

/// <summary>
/// Indicates whether this <see cref="Page{TItem}"/> is empty.
/// </summary>
public bool IsEmpty => Items.Count == 0;
}
46 changes: 44 additions & 2 deletions generators/csharp/codegen/src/asIs/Pager.Template.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,47 @@ public virtual async IAsyncEnumerator<TItem> GetAsyncEnumerator(
}
}

/// <summary>
/// Interface for implementing pagination in two directions.
/// </summary>
/// <typeparam name="TItem">The type of the values.</typeparam>
// ReSharper disable once InconsistentNaming
public interface BiPager<TItem>
{
/// <summary>
/// Get the current <see cref="Page{TItem}"/>.
/// </summary>
public Page<TItem> CurrentPage { get; }

/// <summary>
/// Indicates whether there is a next page.
/// </summary>
public bool HasNextPage { get; }

/// <summary>
/// Indicates whether there is a previous page.
/// </summary>
public bool HasPreviousPage { get; }

/// <summary>
/// Get the next <see cref="Page{TItem}"/>.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>
/// The next <see cref="Page{TItem}"/>.
/// </returns>
public Task<Page<TItem>> GetNextPageAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Get the previous <see cref="Page{TItem}"/>.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>
/// The previous <see cref="Page{TItem}"/>.
/// </returns>
public Task<Page<TItem>> GetPreviousPageAsync(CancellationToken cancellationToken = default);
}

internal sealed class OffsetPager<TRequest, TRequestOptions, TResponse, TOffset, TStep, TItem>
: Pager<TItem>
{
Expand Down Expand Up @@ -97,10 +138,11 @@ public override async IAsyncEnumerable<Page<TItem>> AsPagesAsync(
)
{
var hasStep = false;
if(_getStep is not null)
if (_getStep is not null)
{
hasStep = _getStep(_request) is not null;
}

var offset = _getOffset(_request);
var longOffset = Convert.ToInt64(offset);
bool hasNextPage;
Expand Down Expand Up @@ -204,4 +246,4 @@ public override async IAsyncEnumerable<Page<TItem>> AsPagesAsync(
_setCursor(_request, nextCursor);
} while (true);
}
}
}
Loading
Loading