From 339cc013501438ba9fb2c0c3c2f7847ae3f525ea Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:05:14 -0500 Subject: [PATCH] feat(csharp): add pagination methods to C# SDK generator (#5187) feat(csharp): add pagination methods to C# SDK generator --- .vscode/settings.json | 2 +- .../changelogs/csharp-sdk/2024-11-19.mdx | 7 + generators/csharp/codegen/src/AsIs.ts | 74 +-- .../csharp/codegen/src/asIs/Page.Template.cs | 19 + .../csharp/codegen/src/asIs/Pager.Template.cs | 207 +++++++ .../EnumSerializerTests.Template.cs | 0 .../Pagination/GuidCursorTest.Template.cs | 134 +++++ .../HasNextPageOffsetTest.Template.cs | 116 ++++ .../test/Pagination/IntOffsetTest.Template.cs | 112 ++++ .../Pagination/LongOffsetTest.Template.cs | 112 ++++ .../NoRequestCursorTest.Template.cs | 130 +++++ .../NoRequestOffsetTest.Template.cs | 106 ++++ .../Pagination/StepOffsetTest.Template.cs | 127 +++++ .../Pagination/StringCursorTest.Template.cs | 133 +++++ .../{ => test}/RawClientTests.Template.cs | 0 .../StringEnumSerializerTests.Template.cs | 0 .../src/asIs/{ => test}/Template.Test.csproj | 0 .../{ => test}/Test.Custom.props.Template | 0 generators/csharp/codegen/src/ast/Class.ts | 4 + .../codegen/src/project/CsharpProject.ts | 24 +- .../csharp/model/src/ModelGeneratorContext.ts | 4 +- generators/csharp/sdk/package.json | 4 +- .../csharp/sdk/src/SdkGeneratorContext.ts | 67 ++- .../src/endpoint/AbstractEndpointGenerator.ts | 90 +++- .../sdk/src/endpoint/EndpointGenerator.ts | 36 +- .../endpoint/grpc/GrpcEndpointGenerator.ts | 11 + .../endpoint/http/HttpEndpointGenerator.ts | 503 ++++++++++++++++-- .../snippets/EndpointSnippetsGenerator.ts | 53 +- .../endpoint/snippets/SnippetJsonGenerator.ts | 4 +- .../sdk/src/reference/buildReference.ts | 35 +- .../src/root-client/RootClientGenerator.ts | 4 +- .../SubPackageClientGenerator.ts | 4 +- .../mock-server/MockServerTestGenerator.ts | 67 ++- generators/csharp/sdk/versions.yml | 10 + package.json | 2 +- .../pagination/.mock/definition/users.yml | 116 ++-- .../pagination/.mock/definition/users.yml | 116 ++-- seed/csharp-sdk/pagination/reference.md | 20 +- .../Core/Pagination/GuidCursorTest.cs | 107 ++++ .../Core/Pagination/HasNextPageOffsetTest.cs | 94 ++++ .../Core/Pagination/IntOffsetTest.cs | 81 +++ .../Core/Pagination/LongOffsetTest.cs | 81 +++ .../Core/Pagination/NoRequestCursorTest.cs | 107 ++++ .../Core/Pagination/NoRequestOffsetTest.cs | 81 +++ .../Core/Pagination/StepOffsetTest.cs | 96 ++++ .../Core/Pagination/StringCursorTest.cs | 107 ++++ .../Unit/MockServer/ListUsernamesTest.cs | 14 +- .../ListWithBodyCursorPaginationTest.cs | 14 +- .../ListWithBodyOffsetPaginationTest.cs | 14 +- .../ListWithCursorPaginationTest.cs | 14 +- ...tWithExtendedResultsAndOptionalDataTest.cs | 14 +- .../MockServer/ListWithExtendedResultsTest.cs | 14 +- .../MockServer/ListWithGlobalConfigTest.cs | 14 +- ...ListWithOffsetPaginationHasNextPageTest.cs | 14 +- .../ListWithOffsetPaginationTest.cs | 14 +- .../ListWithOffsetStepPaginationTest.cs | 14 +- .../src/SeedPagination/Core/Page.cs | 19 + .../src/SeedPagination/Core/Pager.cs | 207 +++++++ .../src/SeedPagination/Users/UsersClient.cs | 481 ++++++++++++++--- .../fern/apis/pagination/definition/users.yml | 116 ++-- 60 files changed, 3629 insertions(+), 541 deletions(-) create mode 100644 fern/pages/changelogs/csharp-sdk/2024-11-19.mdx create mode 100644 generators/csharp/codegen/src/asIs/Page.Template.cs create mode 100644 generators/csharp/codegen/src/asIs/Pager.Template.cs rename generators/csharp/codegen/src/asIs/{ => test}/EnumSerializerTests.Template.cs (100%) create mode 100644 generators/csharp/codegen/src/asIs/test/Pagination/GuidCursorTest.Template.cs create mode 100644 generators/csharp/codegen/src/asIs/test/Pagination/HasNextPageOffsetTest.Template.cs create mode 100644 generators/csharp/codegen/src/asIs/test/Pagination/IntOffsetTest.Template.cs create mode 100644 generators/csharp/codegen/src/asIs/test/Pagination/LongOffsetTest.Template.cs create mode 100644 generators/csharp/codegen/src/asIs/test/Pagination/NoRequestCursorTest.Template.cs create mode 100644 generators/csharp/codegen/src/asIs/test/Pagination/NoRequestOffsetTest.Template.cs create mode 100644 generators/csharp/codegen/src/asIs/test/Pagination/StepOffsetTest.Template.cs create mode 100644 generators/csharp/codegen/src/asIs/test/Pagination/StringCursorTest.Template.cs rename generators/csharp/codegen/src/asIs/{ => test}/RawClientTests.Template.cs (100%) rename generators/csharp/codegen/src/asIs/{ => test}/StringEnumSerializerTests.Template.cs (100%) rename generators/csharp/codegen/src/asIs/{ => test}/Template.Test.csproj (100%) rename generators/csharp/codegen/src/asIs/{ => test}/Test.Custom.props.Template (100%) create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/GuidCursorTest.cs create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/HasNextPageOffsetTest.cs create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/IntOffsetTest.cs create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/LongOffsetTest.cs create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/NoRequestCursorTest.cs create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/NoRequestOffsetTest.cs create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/StepOffsetTest.cs create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/StringCursorTest.cs create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination/Core/Page.cs create mode 100644 seed/csharp-sdk/pagination/src/SeedPagination/Core/Pager.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index 4dcb3ec9073..3bd17b42d69 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,4 +16,4 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "python.analysis.typeCheckingMode": "basic" -} +} \ No newline at end of file diff --git a/fern/pages/changelogs/csharp-sdk/2024-11-19.mdx b/fern/pages/changelogs/csharp-sdk/2024-11-19.mdx new file mode 100644 index 00000000000..1bb8985b551 --- /dev/null +++ b/fern/pages/changelogs/csharp-sdk/2024-11-19.mdx @@ -0,0 +1,7 @@ +## 1.9.9 +**`(feat):`** Add support for [Auto Pagination](https://buildwithfern.com/learn/sdks/features/auto-pagination). +When enabled, the endpoint methods will return a `Pager` object that you can use to iterate over all items of an endpoint. +Additionally, you can use the `Pager.AsPagesAsync` method to iterate over all pages of an endpoint. +The SDK will automatically make the necessary HTTP requests for you as you iterate over the items or the pages. + + diff --git a/generators/csharp/codegen/src/AsIs.ts b/generators/csharp/codegen/src/AsIs.ts index 791c6b3e0bf..9cbf469d1e9 100644 --- a/generators/csharp/codegen/src/AsIs.ts +++ b/generators/csharp/codegen/src/AsIs.ts @@ -6,33 +6,47 @@ export const DATETIME_SERIALIZER_CLASS_NAME = "DateTimeSerializer"; export const CONSTANTS_CLASS_NAME = "Constants"; export const JSON_UTILS_CLASS_NAME = "JsonUtils"; -export enum AsIsFiles { - GitIgnore = ".gitignore.Template", - CiYaml = "github-ci.yml", - CollectionItemSerializer = "CollectionItemSerializer.cs", - Constants = "Constants.cs", - DateTimeSerializer = "DateTimeSerializer.cs", - EnumConverter = "EnumConverter.Template.cs", - GrpcRequestOptions = "GrpcRequestOptions.Template.cs", - Headers = "Headers.Template.cs", - HeaderValue = "HeaderValue.Template.cs", - HttpMethodExtensions = "HttpMethodExtensions.cs", - JsonConfiguration = "JsonConfiguration.cs", - OneOfSerializer = "OneOfSerializer.cs", - RawClient = "RawClient.Template.cs", - RawClientTests = "RawClientTests.Template.cs", - RawGrpcClient = "RawGrpcClient.Template.cs", - EnumSerializer = "EnumSerializer.Template.cs", - EnumSerializerTests = "EnumSerializerTests.Template.cs", - StringEnum = "StringEnum.Template.cs", - StringEnumExtensions = "StringEnumExtensions.Template.cs", - StringEnumSerializer = "StringEnumSerializer.Template.cs", - StringEnumSerializerTests = "StringEnumSerializerTests.Template.cs", - TemplateCsProj = "Template.csproj", - TemplateTestCsProj = "Template.Test.csproj", - TemplateTestClientCs = "TemplateTestClient.cs", - UsingCs = "Using.cs", - Extensions = "Extensions.cs", - CustomProps = "Custom.props.Template", - TestCustomProps = "Test.Custom.props.Template" -} +export const AsIsFiles = { + CiYaml: "github-ci.yml", + CollectionItemSerializer: "CollectionItemSerializer.cs", + Constants: "Constants.cs", + CustomProps: "Custom.props.Template", + DateTimeSerializer: "DateTimeSerializer.cs", + EnumConverter: "EnumConverter.Template.cs", + EnumSerializer: "EnumSerializer.Template.cs", + Extensions: "Extensions.cs", + GitIgnore: ".gitignore.Template", + GrpcRequestOptions: "GrpcRequestOptions.Template.cs", + Headers: "Headers.Template.cs", + HeaderValue: "HeaderValue.Template.cs", + HttpMethodExtensions: "HttpMethodExtensions.cs", + JsonConfiguration: "JsonConfiguration.cs", + OneOfSerializer: "OneOfSerializer.cs", + Page: "Page.Template.cs", + Pager: "Pager.Template.cs", + RawClient: "RawClient.Template.cs", + RawGrpcClient: "RawGrpcClient.Template.cs", + StringEnum: "StringEnum.Template.cs", + StringEnumExtensions: "StringEnumExtensions.Template.cs", + StringEnumSerializer: "StringEnumSerializer.Template.cs", + TemplateCsProj: "Template.csproj", + UsingCs: "Using.cs", + Test: { + TestCustomProps: "test/Test.Custom.props.Template", + TemplateTestClientCs: "test/TemplateTestClient.cs", + TemplateTestCsProj: "test/Template.Test.csproj", + EnumSerializerTests: "test/EnumSerializerTests.Template.cs", + RawClientTests: "test/RawClientTests.Template.cs", + StringEnumSerializerTests: "test/StringEnumSerializerTests.Template.cs", + Pagination: [ + "test/Pagination/GuidCursorTest.Template.cs", + "test/Pagination/HasNextPageOffsetTest.Template.cs", + "test/Pagination/IntOffsetTest.Template.cs", + "test/Pagination/LongOffsetTest.Template.cs", + "test/Pagination/NoRequestCursorTest.Template.cs", + "test/Pagination/NoRequestOffsetTest.Template.cs", + "test/Pagination/StepOffsetTest.Template.cs", + "test/Pagination/StringCursorTest.Template.cs" + ] + } +}; diff --git a/generators/csharp/codegen/src/asIs/Page.Template.cs b/generators/csharp/codegen/src/asIs/Page.Template.cs new file mode 100644 index 00000000000..ce2b0222752 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/Page.Template.cs @@ -0,0 +1,19 @@ +namespace <%= namespace%>; + +/// +/// A single of items from a request that may return +/// zero or more s of items. +/// +/// The type of items. +public class Page +{ + public Page(IReadOnlyList items) + { + Items = items; + } + + /// + /// Gets the items in this . + /// + public IReadOnlyList Items { get; } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/Pager.Template.cs b/generators/csharp/codegen/src/asIs/Pager.Template.cs new file mode 100644 index 00000000000..8c53f8caeb3 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/Pager.Template.cs @@ -0,0 +1,207 @@ +using System.Runtime.CompilerServices; + +namespace <%= namespace%>; + +/// +/// A collection of values that may take multiple service requests to +/// iterate over. +/// +/// The type of the values. +public abstract class Pager : IAsyncEnumerable +{ + /// + /// Enumerate the values a at a time. This may + /// make multiple service requests. + /// + /// + /// An async sequence of s. + /// + public abstract IAsyncEnumerable> AsPagesAsync( + CancellationToken cancellationToken = default + ); + + /// + /// Enumerate the values in the collection asynchronously. This may + /// make multiple service requests. + /// + /// + /// The used for requests made while + /// enumerating asynchronously. + /// + /// An async sequence of values. + public virtual async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) + { + await foreach (var page in AsPagesAsync(cancellationToken).ConfigureAwait(false)) + { + foreach (var value in page.Items) + { + yield return value; + } + } + } +} + +internal sealed class OffsetPager + : Pager +{ + private TRequest _request; + private readonly TRequestOptions? _options; + private readonly GetNextPage _getNextPage; + private readonly GetOffset _getOffset; + private readonly SetOffset _setOffset; + private readonly GetStep? _getStep; + private readonly GetItems _getItems; + private readonly HasNextPage? _hasNextPage; + + internal delegate Task GetNextPage( + TRequest request, + TRequestOptions? options, + CancellationToken cancellationToken + ); + + internal delegate TOffset GetOffset(TRequest request); + + internal delegate void SetOffset(TRequest request, TOffset offset); + + internal delegate TStep GetStep(TRequest request); + + internal delegate IReadOnlyList? GetItems(TResponse response); + + internal delegate bool? HasNextPage(TResponse response); + + internal OffsetPager( + TRequest request, + TRequestOptions? options, + GetNextPage getNextPage, + GetOffset getOffset, + SetOffset setOffset, + GetStep? getStep, + GetItems getItems, + HasNextPage? hasNextPage + ) + { + _request = request; + _options = options; + _getNextPage = getNextPage; + _getOffset = getOffset; + _setOffset = setOffset; + _getStep = getStep; + _getItems = getItems; + _hasNextPage = hasNextPage; + } + + public override async IAsyncEnumerable> AsPagesAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + var hasStep = false; + if(_getStep is not null) + { + hasStep = _getStep(_request) is not null; + } + var offset = _getOffset(_request); + var longOffset = Convert.ToInt64(offset); + bool hasNextPage; + do + { + var response = await _getNextPage(_request, _options, cancellationToken) + .ConfigureAwait(false); + var items = _getItems(response); + var itemCount = items?.Count ?? 0; + hasNextPage = _hasNextPage?.Invoke(response) ?? itemCount > 0; + if (items is not null) + { + yield return new Page(items); + } + + if (hasStep) + { + longOffset += items?.Count ?? 1; + } + else + { + longOffset++; + } + + _request ??= Activator.CreateInstance(); + switch (offset) + { + case int: + _setOffset(_request, (TOffset)(object)(int)longOffset); + break; + case long: + _setOffset(_request, (TOffset)(object)longOffset); + break; + default: + throw new InvalidOperationException("Offset must be int or long"); + } + } while (hasNextPage); + } +} + +internal sealed class CursorPager + : Pager +{ + private TRequest _request; + private readonly TRequestOptions? _options; + private readonly GetNextPage _getNextPage; + private readonly SetCursor _setCursor; + private readonly GetNextCursor _getNextCursor; + private readonly GetItems _getItems; + + internal delegate Task GetNextPage( + TRequest request, + TRequestOptions? options, + CancellationToken cancellationToken + ); + + internal delegate void SetCursor(TRequest request, TCursor cursor); + + internal delegate TCursor? GetNextCursor(TResponse response); + + internal delegate IReadOnlyList? GetItems(TResponse response); + + internal CursorPager( + TRequest request, + TRequestOptions? options, + GetNextPage getNextPage, + SetCursor setCursor, + GetNextCursor getNextCursor, + GetItems getItems + ) + { + _request = request; + _options = options; + _getNextPage = getNextPage; + _setCursor = setCursor; + _getNextCursor = getNextCursor; + _getItems = getItems; + } + + public override async IAsyncEnumerable> AsPagesAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + do + { + var response = await _getNextPage(_request, _options, cancellationToken) + .ConfigureAwait(false); + var items = _getItems(response); + var nextCursor = _getNextCursor(response); + if (items != null) + { + yield return new Page(items); + } + + if (nextCursor == null) + { + break; + } + + _request ??= Activator.CreateInstance(); + _setCursor(_request, nextCursor); + } while (true); + } +} diff --git a/generators/csharp/codegen/src/asIs/EnumSerializerTests.Template.cs b/generators/csharp/codegen/src/asIs/test/EnumSerializerTests.Template.cs similarity index 100% rename from generators/csharp/codegen/src/asIs/EnumSerializerTests.Template.cs rename to generators/csharp/codegen/src/asIs/test/EnumSerializerTests.Template.cs diff --git a/generators/csharp/codegen/src/asIs/test/Pagination/GuidCursorTest.Template.cs b/generators/csharp/codegen/src/asIs/test/Pagination/GuidCursorTest.Template.cs new file mode 100644 index 00000000000..340b1bc5bd7 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/test/Pagination/GuidCursorTest.Template.cs @@ -0,0 +1,134 @@ +using NUnit.Framework; +using <%= namespace%>.Core; + +namespace <%= namespace%>.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class GuidCursorTest +{ + [Test] + public async Task CursorPagerShouldWorkWithGuidCursors() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private static readonly Guid? Cursor1 = null; + private static readonly Guid Cursor2 = new("00000000-0000-0000-0000-000000000001"); + private static readonly Guid Cursor3 = new("00000000-0000-0000-0000-000000000001"); + private Guid? _cursorCopy; + + private Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() + { + Items = ["item1", "item2"] + }, + Cursor = new() + { + Next = Cursor2 + } + }, + new() + { + Data = new() + { + Items = ["item1"] + }, + Cursor = new() + { + Next = Cursor3 + } + }, + new() + { + Data = new() + { + Items = [] + }, + Cursor = new() + { + Next = null + } + } + }.GetEnumerator(); + _cursorCopy = Cursor1; + Pager pager = new CursorPager< + Request, + object?, + Response, + Guid?, + object + >( + new() + { + Cursor = Cursor1 + }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + (request, cursor) => + { + request.Cursor = cursor; + _cursorCopy = cursor; + }, + response => response?.Cursor?.Next, + response => response?.Data?.Items?.ToList() + ); + return pager; + } + + private async Task AssertPager(Pager pager) + { + var pageEnumerator = pager.AsPagesAsync().GetAsyncEnumerator(); + + // first page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + var page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(2)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor1)); + + // second page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(1)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor2)); + + // third page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(0)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor3)); + + // no more + Assert.That(await pageEnumerator.MoveNextAsync(), Is.False); + } + + private class Request + { + public required Guid? Cursor { get; set; } + } + + private class Response + { + public required Data Data { get; set; } + public required Cursor Cursor { get; set; } + } + + private class Data + { + public required IEnumerable Items { get; set; } + } + + private class Cursor + { + public required Guid? Next { get; set; } + } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/test/Pagination/HasNextPageOffsetTest.Template.cs b/generators/csharp/codegen/src/asIs/test/Pagination/HasNextPageOffsetTest.Template.cs new file mode 100644 index 00000000000..769139df972 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/test/Pagination/HasNextPageOffsetTest.Template.cs @@ -0,0 +1,116 @@ +using NUnit.Framework; +using <%= namespace%>.Core; + +namespace <%= namespace%>.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class HasNextPageOffsetTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithHasNextPage() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private static Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() + { + Items = ["item1", "item2"] + }, + HasNext = true + }, + new() + { + Data = new() + { + Items = ["item1", "item2"] + }, + HasNext = true + }, + new() + { + Data = new() + { + Items = ["item1",] + }, + HasNext = false + } + }.GetEnumerator(); + Pager pager = new OffsetPager< + Request, + object?, + Response, + int, + object?, + object + >( + new() + { + Pagination = new() + { + Page = 1 + } + }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request?.Pagination?.Page ?? 0, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.Page = offset; + }, + null, + response => response?.Data?.Items?.ToList(), + response => response.HasNext + ); + return pager; + } + + private static async Task AssertPager(Pager pager) + { + var pageCounter = 0; + var itemCounter = 0; + await foreach (var page in pager.AsPagesAsync()) + { + pageCounter++; + itemCounter += page.Items.Count; + } + + Assert.Multiple(() => + { + Assert.That(pageCounter, Is.EqualTo(3)); + Assert.That(itemCounter, Is.EqualTo(5)); + }); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public int Page { get; set; } + } + + private class Response + { + public Data Data { get; set; } + public bool HasNext { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/test/Pagination/IntOffsetTest.Template.cs b/generators/csharp/codegen/src/asIs/test/Pagination/IntOffsetTest.Template.cs new file mode 100644 index 00000000000..9e2edd86f3c --- /dev/null +++ b/generators/csharp/codegen/src/asIs/test/Pagination/IntOffsetTest.Template.cs @@ -0,0 +1,112 @@ +using NUnit.Framework; +using <%= namespace%>.Core; + +namespace <%= namespace%>.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class IntOffsetTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithIntPage() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + public Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() + { + Items = ["item1", "item2"] + } + }, + new() + { + Data = new() + { + Items = ["item1"] + } + }, + new() + { + Data = new() + { + Items = [] + } + } + }.GetEnumerator(); + Pager pager = new OffsetPager< + Request, + object?, + Response, + int, + object?, + object + >( + new() + { + Pagination = new() + { + Page = 1 + } + }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request.Pagination.Page, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.Page = offset; + }, + null, + response => response?.Data?.Items?.ToList(), + null + ); + return pager; + } + + public async Task AssertPager(Pager pager) + { + var pageCounter = 0; + var itemCounter = 0; + await foreach (var page in pager.AsPagesAsync()) + { + pageCounter++; + itemCounter += page.Items.Count; + } + + Assert.Multiple(() => + { + Assert.That(pageCounter, Is.EqualTo(3)); + Assert.That(itemCounter, Is.EqualTo(3)); + }); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public int Page { get; set; } + } + + private class Response + { + public Data Data { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/test/Pagination/LongOffsetTest.Template.cs b/generators/csharp/codegen/src/asIs/test/Pagination/LongOffsetTest.Template.cs new file mode 100644 index 00000000000..02960048e76 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/test/Pagination/LongOffsetTest.Template.cs @@ -0,0 +1,112 @@ +using NUnit.Framework; +using <%= namespace%>.Core; + +namespace <%= namespace%>.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class LongOffsetTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithLongPage() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private static Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() + { + Items = ["item1", "item2"] + } + }, + new() + { + Data = new() + { + Items = ["item1"] + } + }, + new() + { + Data = new() + { + Items = [] + } + } + }.GetEnumerator(); + Pager pager = new OffsetPager< + Request, + object?, + Response, + long, + object?, + object + >( + new() + { + Pagination = new() + { + Page = 1 + } + }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request?.Pagination?.Page ?? 0, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.Page = offset; + }, + null, + response => response?.Data?.Items?.ToList(), + null + ); + return pager; + } + + private static async Task AssertPager(Pager pager) + { + var pageCounter = 0; + var itemCounter = 0; + await foreach (var page in pager.AsPagesAsync()) + { + pageCounter++; + itemCounter += page.Items.Count; + } + + Assert.Multiple(() => + { + Assert.That(pageCounter, Is.EqualTo(3)); + Assert.That(itemCounter, Is.EqualTo(3)); + }); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public long Page { get; set; } + } + + private class Response + { + public Data Data { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/test/Pagination/NoRequestCursorTest.Template.cs b/generators/csharp/codegen/src/asIs/test/Pagination/NoRequestCursorTest.Template.cs new file mode 100644 index 00000000000..778cfd03534 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/test/Pagination/NoRequestCursorTest.Template.cs @@ -0,0 +1,130 @@ +using NUnit.Framework; +using <%= namespace%>.Core; + +namespace <%= namespace%>.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class NoRequestCursorTest +{ + [Test] + public async Task CursorPagerShouldWorkWithStringCursor() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private const string? Cursor1 = null; + private const string Cursor2 = "cursor2"; + private const string Cursor3 = "cursor3"; + private string? _cursorCopy; + private Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() + { + Items = ["item1", "item2"] + }, + Cursor = new () + { + Next = Cursor2 + } + }, + new() + { + Data = new() + { + Items = ["item1"] + }, + Cursor = new () + { + Next = Cursor3 + } + }, + new() + { + Data = new() + { + Items = [] + }, + Cursor = new () + { + Next = null + } + } + }.GetEnumerator(); + _cursorCopy = Cursor1; + Pager pager = new CursorPager< + Request?, + object?, + Response, + string, + object + >( + null, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + (request, cursor) => + { + request.Cursor = cursor; + _cursorCopy = cursor; + }, + response => response?.Cursor?.Next, + response => response?.Data?.Items?.ToList() + ); + return pager; + } + + private async Task AssertPager(Pager pager) + { + var pageEnumerator = pager.AsPagesAsync().GetAsyncEnumerator(); + + // first page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + var page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(2)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor1)); + + // second page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(1)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor2)); + + // third page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(0)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor3)); + + // no more + Assert.That(await pageEnumerator.MoveNextAsync(), Is.False); + } + + private class Request + { + public required string? Cursor { get; set; } + } + + private class Response + { + public required Data Data { get; set; } + public required Cursor Cursor { get; set; } + } + + private class Data + { + public required IEnumerable Items { get; set; } + } + + private class Cursor + { + public required string? Next { get; set; } + } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/test/Pagination/NoRequestOffsetTest.Template.cs b/generators/csharp/codegen/src/asIs/test/Pagination/NoRequestOffsetTest.Template.cs new file mode 100644 index 00000000000..5d6543f0931 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/test/Pagination/NoRequestOffsetTest.Template.cs @@ -0,0 +1,106 @@ +using NUnit.Framework; +using <%= namespace%>.Core; + +namespace <%= namespace%>.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class NoRequestOffsetTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithoutRequest() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + public Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() + { + Items = ["item1", "item2"] + } + }, + new() + { + Data = new() + { + Items = ["item1"] + } + }, + new() + { + Data = new() + { + Items = [] + } + } + }.GetEnumerator(); + Pager pager = new OffsetPager< + Request?, + object?, + Response, + int, + object?, + object + >( + null, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request?.Pagination?.Page ?? 0, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.Page = offset; + }, + null, + response => response?.Data?.Items?.ToList(), + null + ); + return pager; + } + + public async Task AssertPager(Pager pager) + { + var pageCounter = 0; + var itemCounter = 0; + await foreach (var page in pager.AsPagesAsync()) + { + pageCounter++; + itemCounter += page.Items.Count; + } + + Assert.Multiple(() => + { + Assert.That(pageCounter, Is.EqualTo(3)); + Assert.That(itemCounter, Is.EqualTo(3)); + }); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public int Page { get; set; } + } + + private class Response + { + public Data Data { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/test/Pagination/StepOffsetTest.Template.cs b/generators/csharp/codegen/src/asIs/test/Pagination/StepOffsetTest.Template.cs new file mode 100644 index 00000000000..8327e366b22 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/test/Pagination/StepOffsetTest.Template.cs @@ -0,0 +1,127 @@ +using NUnit.Framework; +using <%= namespace%>.Core; + +namespace <%= namespace%>.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class StepPageOffsetPaginationTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithStep() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private Pagination _paginationCopy; + private Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() + { + Items = ["item1", "item2"] + } + }, + new() + { + Data = new() + { + Items = ["item1"] + } + }, + new() + { + Data = new() + { + Items = [] + } + } + }.GetEnumerator(); + _paginationCopy = new() + { + ItemOffset = 0, + PageSize = 2 + }; + Pager pager = new OffsetPager< + Request, + object?, + Response, + int, + object?, + object + >( + new() + { + Pagination = _paginationCopy + }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request?.Pagination?.ItemOffset ?? 0, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.ItemOffset = offset; + _paginationCopy = request.Pagination; + }, + request => request?.Pagination?.PageSize, + response => response?.Data?.Items?.ToList(), + null + ); + return pager; + } + + private async Task AssertPager(Pager pager) + { + var pageEnumerator = pager.AsPagesAsync().GetAsyncEnumerator(); + + // first page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + var page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(2)); + Assert.That(_paginationCopy.ItemOffset, Is.EqualTo(0)); + + // second page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(1)); + Assert.That(_paginationCopy.ItemOffset, Is.EqualTo(2)); + + // third page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(0)); + Assert.That(_paginationCopy.ItemOffset, Is.EqualTo(3)); + + // no more + Assert.That(await pageEnumerator.MoveNextAsync(), Is.False); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public int ItemOffset { get; set; } + public int PageSize { get; set; } + } + + private class Response + { + public Data Data { get; set; } + public bool HasNext { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/test/Pagination/StringCursorTest.Template.cs b/generators/csharp/codegen/src/asIs/test/Pagination/StringCursorTest.Template.cs new file mode 100644 index 00000000000..f84e89fd5e8 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/test/Pagination/StringCursorTest.Template.cs @@ -0,0 +1,133 @@ +using NUnit.Framework; +using <%= namespace%>.Core; + +namespace <%= namespace%>.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class StringCursorTest +{ + [Test] + public async Task CursorPagerShouldWorkWithStringCursor() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private const string? Cursor1 = null; + private const string Cursor2 = "cursor2"; + private const string Cursor3 = "cursor3"; + private string? _cursorCopy; + private Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() + { + Items = ["item1", "item2"] + }, + Cursor = new () + { + Next = Cursor2 + } + }, + new() + { + Data = new() + { + Items = ["item1"] + }, + Cursor = new () + { + Next = Cursor3 + } + }, + new() + { + Data = new() + { + Items = [] + }, + Cursor = new () + { + Next = null + } + } + }.GetEnumerator(); + _cursorCopy = Cursor1; + Pager pager = new CursorPager< + Request, + object?, + Response, + string, + object + >( + new() + { + Cursor = Cursor1 + }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + (request, cursor) => + { + request.Cursor = cursor; + _cursorCopy = cursor; + }, + response => response?.Cursor?.Next, + response => response?.Data?.Items?.ToList() + ); + return pager; + } + + private async Task AssertPager(Pager pager) + { + var pageEnumerator = pager.AsPagesAsync().GetAsyncEnumerator(); + + // first page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + var page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(2)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor1)); + + // second page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(1)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor2)); + + // third page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(0)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor3)); + + // no more + Assert.That(await pageEnumerator.MoveNextAsync(), Is.False); + } + + private class Request + { + public required string? Cursor { get; set; } + } + + private class Response + { + public required Data Data { get; set; } + public required Cursor Cursor { get; set; } + } + + private class Data + { + public required IEnumerable Items { get; set; } + } + + private class Cursor + { + public required string? Next { get; set; } + } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/RawClientTests.Template.cs b/generators/csharp/codegen/src/asIs/test/RawClientTests.Template.cs similarity index 100% rename from generators/csharp/codegen/src/asIs/RawClientTests.Template.cs rename to generators/csharp/codegen/src/asIs/test/RawClientTests.Template.cs diff --git a/generators/csharp/codegen/src/asIs/StringEnumSerializerTests.Template.cs b/generators/csharp/codegen/src/asIs/test/StringEnumSerializerTests.Template.cs similarity index 100% rename from generators/csharp/codegen/src/asIs/StringEnumSerializerTests.Template.cs rename to generators/csharp/codegen/src/asIs/test/StringEnumSerializerTests.Template.cs diff --git a/generators/csharp/codegen/src/asIs/Template.Test.csproj b/generators/csharp/codegen/src/asIs/test/Template.Test.csproj similarity index 100% rename from generators/csharp/codegen/src/asIs/Template.Test.csproj rename to generators/csharp/codegen/src/asIs/test/Template.Test.csproj diff --git a/generators/csharp/codegen/src/asIs/Test.Custom.props.Template b/generators/csharp/codegen/src/asIs/test/Test.Custom.props.Template similarity index 100% rename from generators/csharp/codegen/src/asIs/Test.Custom.props.Template rename to generators/csharp/codegen/src/asIs/test/Test.Custom.props.Template diff --git a/generators/csharp/codegen/src/ast/Class.ts b/generators/csharp/codegen/src/ast/Class.ts index 1264e5dc81a..3c093b6dd1a 100644 --- a/generators/csharp/codegen/src/ast/Class.ts +++ b/generators/csharp/codegen/src/ast/Class.ts @@ -170,6 +170,10 @@ export class Class extends AstNode { this.methods.push(method); } + public addMethods(methods: Method[]): void { + methods.forEach((method) => this.addMethod(method)); + } + public addNestedClass(subClass: Class): void { this.nestedClasses.push(subClass); } diff --git a/generators/csharp/codegen/src/project/CsharpProject.ts b/generators/csharp/codegen/src/project/CsharpProject.ts index f2970461159..eec77719c8e 100644 --- a/generators/csharp/codegen/src/project/CsharpProject.ts +++ b/generators/csharp/codegen/src/project/CsharpProject.ts @@ -107,7 +107,7 @@ export class CsharpProject extends AbstractProject { + const contents = (await readFile(getAsIsFilepath(filename))).toString(); + return new File( + filename.replace("test/", "").replace(".Template", ""), + RelativeFilePath.of(""), + replaceTemplate({ + contents, + grpc: this.context.hasGrpcEndpoints(), + idempotencyHeaders: this.context.hasIdempotencyHeaders(), + namespace + }) + ); + } + private async createAsIsFile({ filename, namespace }: { filename: string; namespace: string }): Promise { const contents = (await readFile(getAsIsFilepath(filename))).toString(); return new File( diff --git a/generators/csharp/model/src/ModelGeneratorContext.ts b/generators/csharp/model/src/ModelGeneratorContext.ts index 4e437db6016..48184491762 100644 --- a/generators/csharp/model/src/ModelGeneratorContext.ts +++ b/generators/csharp/model/src/ModelGeneratorContext.ts @@ -51,9 +51,9 @@ export class ModelGeneratorContext extends AbstractCsharpGeneratorContext files.push(file)); } return files; } @@ -492,6 +503,56 @@ export class SdkGeneratorContext extends AbstractCsharpGeneratorContext endpoint.id === endpointId); if (httpEndpoint == null) { diff --git a/generators/csharp/sdk/src/endpoint/AbstractEndpointGenerator.ts b/generators/csharp/sdk/src/endpoint/AbstractEndpointGenerator.ts index c5a94675856..10107c539d8 100644 --- a/generators/csharp/sdk/src/endpoint/AbstractEndpointGenerator.ts +++ b/generators/csharp/sdk/src/endpoint/AbstractEndpointGenerator.ts @@ -6,6 +6,9 @@ import { WrappedRequestGenerator } from "../wrapped-request/WrappedRequestGenera import { getEndpointRequest } from "./utils/getEndpointRequest"; import { getEndpointReturnType } from "./utils/getEndpointReturnType"; import { EndpointSignatureInfo } from "./EndpointSignatureInfo"; +import { assertNever } from "@fern-api/core-utils"; + +type PagingEndpoint = HttpEndpoint & { pagination: NonNullable }; export abstract class AbstractEndpointGenerator { private exampleGenerator: ExampleGenerator; @@ -22,6 +25,18 @@ export abstract class AbstractEndpointGenerator { }: { serviceId: ServiceId; endpoint: HttpEndpoint; + }): EndpointSignatureInfo { + return this.hasPagination(endpoint) + ? this.getPagerEndpointSignatureInfo({ serviceId, endpoint }) + : this.getUnpagedEndpointSignatureInfo({ serviceId, endpoint }); + } + + protected getUnpagedEndpointSignatureInfo({ + serviceId, + endpoint + }: { + serviceId: ServiceId; + endpoint: HttpEndpoint; }): EndpointSignatureInfo { const { pathParameters, pathParameterReferences } = this.getAllPathParameters({ serviceId, endpoint }); const request = getEndpointRequest({ context: this.context, endpoint, serviceId }); @@ -39,7 +54,62 @@ export abstract class AbstractEndpointGenerator { }; } - private getAllPathParameters({ + protected getPagerEndpointSignatureInfo({ + serviceId, + endpoint + }: { + serviceId: ServiceId; + endpoint: HttpEndpoint; + }): EndpointSignatureInfo { + const { pathParameters, pathParameterReferences } = this.getAllPathParameters({ serviceId, endpoint }); + const request = getEndpointRequest({ context: this.context, endpoint, serviceId }); + const requestParameter = + request != null + ? csharp.parameter({ type: request.getParameterType(), name: request.getParameterName() }) + : undefined; + return { + baseParameters: [...pathParameters, requestParameter].filter((p): p is csharp.Parameter => p != null), + pathParameters, + pathParameterReferences, + request, + requestParameter, + returnType: this.getPagerReturnType(endpoint) + }; + } + + protected getPagerReturnType(endpoint: HttpEndpoint): csharp.Type { + const itemType = this.getPaginationItemType(endpoint); + const pager = this.context.getPagerClassReference({ + itemType + }); + return csharp.Type.reference(pager); + } + + protected getPaginationItemType(endpoint: HttpEndpoint): csharp.Type { + this.assertHasPagination(endpoint); + const listItemType = this.context.csharpTypeMapper.convert({ + reference: (() => { + switch (endpoint.pagination.type) { + case "offset": + return endpoint.pagination.results.property.valueType; + case "cursor": + return endpoint.pagination.results.property.valueType; + default: + assertNever(endpoint.pagination); + } + })(), + unboxOptionals: true + }); + + if (listItemType.internalType.type !== "list") { + throw new Error( + `Pagination result type for endpoint ${endpoint.name.originalName} must be a list, but is ${listItemType.internalType.type}.` + ); + } + return listItemType.internalType.value; + } + + protected getAllPathParameters({ serviceId, endpoint }: { @@ -70,6 +140,20 @@ export abstract class AbstractEndpointGenerator { }; } + protected hasPagination(endpoint: HttpEndpoint): endpoint is PagingEndpoint { + if (!this.context.config.generatePaginatedClients) { + return false; + } + return endpoint.pagination !== undefined; + } + + protected assertHasPagination(endpoint: HttpEndpoint): asserts endpoint is PagingEndpoint { + if (this.hasPagination(endpoint)) { + return; + } + throw new Error(`Endpoint ${endpoint.name.originalName} is not a paginated endpoint`); + } + protected generateEndpointSnippet({ example, endpoint, @@ -121,7 +205,7 @@ export abstract class AbstractEndpointGenerator { }); } - private getEndpointRequestSnippet( + protected getEndpointRequestSnippet( exampleEndpointCall: ExampleEndpointCall, endpoint: HttpEndpoint, serviceId: ServiceId, @@ -158,7 +242,7 @@ export abstract class AbstractEndpointGenerator { }); } - private getNonEndpointArguments(example: ExampleEndpointCall, parseDatetimes: boolean): csharp.CodeBlock[] { + protected getNonEndpointArguments(example: ExampleEndpointCall, parseDatetimes: boolean): csharp.CodeBlock[] { const pathParameters = [ ...example.rootPathParameters, ...example.servicePathParameters, diff --git a/generators/csharp/sdk/src/endpoint/EndpointGenerator.ts b/generators/csharp/sdk/src/endpoint/EndpointGenerator.ts index c4fa2c21206..9631084ef6e 100644 --- a/generators/csharp/sdk/src/endpoint/EndpointGenerator.ts +++ b/generators/csharp/sdk/src/endpoint/EndpointGenerator.ts @@ -31,22 +31,32 @@ export class EndpointGenerator extends AbstractEndpointGenerator { rawGrpcClientReference: string; rawClient: RawClient; grpcClientInfo: GrpcClientInfo | undefined; - }): csharp.Method { - // If the service is a grpc service, grpcClientInfo will not be null or undefined, - // so any endpoint will be generated as a grpc endpoint, unless the transport is overriden by setting type to http - if (grpcClientInfo != null && endpoint.transport?.type !== "http") { - return this.grpc.generate({ + }): csharp.Method[] { + if (this.isGrpcEndpoint(grpcClientInfo, endpoint)) { + return [ + this.grpc.generate({ + serviceId, + endpoint, + rawGrpcClientReference, + grpcClientInfo + }) + ]; + } else { + return this.http.generate({ serviceId, endpoint, - rawGrpcClientReference, - grpcClientInfo + rawClientReference, + rawClient }); } - return this.http.generate({ - serviceId, - endpoint, - rawClientReference, - rawClient - }); + } + + private isGrpcEndpoint( + grpcClientInfo: GrpcClientInfo | undefined, + endpoint: HttpEndpoint + ): grpcClientInfo is GrpcClientInfo { + // If the service is a grpc service, grpcClientInfo will not be null or undefined, + // so any endpoint will be generated as a grpc endpoint, unless the transport is overriden by setting type to http + return grpcClientInfo != null && endpoint.transport?.type !== "http"; } } diff --git a/generators/csharp/sdk/src/endpoint/grpc/GrpcEndpointGenerator.ts b/generators/csharp/sdk/src/endpoint/grpc/GrpcEndpointGenerator.ts index c5dbd76d608..bf1533e695f 100644 --- a/generators/csharp/sdk/src/endpoint/grpc/GrpcEndpointGenerator.ts +++ b/generators/csharp/sdk/src/endpoint/grpc/GrpcEndpointGenerator.ts @@ -3,6 +3,7 @@ import { ExampleEndpointCall, HttpEndpoint, ServiceId } from "@fern-fern/ir-sdk/ import { GrpcClientInfo } from "../../grpc/GrpcClientInfo"; import { SdkGeneratorContext } from "../../SdkGeneratorContext"; import { AbstractEndpointGenerator } from "../AbstractEndpointGenerator"; +import { EndpointSignatureInfo } from "../EndpointSignatureInfo"; import { EndpointRequest } from "../request/EndpointRequest"; import { RESPONSE_VARIABLE_NAME } from "../utils/constants"; @@ -252,4 +253,14 @@ export class GrpcEndpointGenerator extends AbstractEndpointGenerator { namespace: "Grpc.Core" }); } + + public getEndpointSignatureInfo({ + serviceId, + endpoint + }: { + serviceId: string; + endpoint: HttpEndpoint; + }): EndpointSignatureInfo { + return super.getUnpagedEndpointSignatureInfo({ serviceId, endpoint }); + } } diff --git a/generators/csharp/sdk/src/endpoint/http/HttpEndpointGenerator.ts b/generators/csharp/sdk/src/endpoint/http/HttpEndpointGenerator.ts index 359f777fc16..925a1bfaefe 100644 --- a/generators/csharp/sdk/src/endpoint/http/HttpEndpointGenerator.ts +++ b/generators/csharp/sdk/src/endpoint/http/HttpEndpointGenerator.ts @@ -1,10 +1,20 @@ import { csharp } from "@fern-api/csharp-codegen"; -import { ExampleEndpointCall, HttpEndpoint, ResponseError, ServiceId } from "@fern-fern/ir-sdk/api"; +import { + CursorPagination, + ExampleEndpointCall, + HttpEndpoint, + OffsetPagination, + RequestProperty, + ResponseError, + ResponseProperty, + ServiceId +} from "@fern-fern/ir-sdk/api"; import { RawClient } from "./RawClient"; import { SdkGeneratorContext } from "../../SdkGeneratorContext"; import { getEndpointReturnType } from "../utils/getEndpointReturnType"; -import { getEndpointRequest } from "../utils/getEndpointRequest"; import { AbstractEndpointGenerator } from "../AbstractEndpointGenerator"; +import { SingleEndpointSnippet } from "../snippets/EndpointSnippetsGenerator"; +import { assertNever } from "@fern-api/core-utils"; export declare namespace EndpointGenerator { export interface Args { @@ -35,28 +45,47 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { endpoint: HttpEndpoint; rawClientReference: string; rawClient: RawClient; + }): csharp.Method[] { + const methods: csharp.Method[] = [ + this.generateHttpMethod({ serviceId, endpoint, rawClientReference, rawClient }) + ]; + if (this.hasPagination(endpoint)) { + methods.push(this.generateHttpPagerMethod({ serviceId, endpoint })); + } + return methods; + } + + private getHttpMethodSnippet({ endpoint }: { endpoint: HttpEndpoint }): SingleEndpointSnippet | undefined { + // if this is a paginated endpoint, don't return a snippet for the internal method + if (this.hasPagination(endpoint)) { + return undefined; + } + const endpointSnippets = this.context.snippetGenerator.getSnippetsForEndpoint(endpoint.id); + const snippet = endpointSnippets?.userSpecified[0] ?? endpointSnippets?.autogenerated[0]; + return snippet; + } + + private getHttpPagerMethodSnippet({ endpoint }: { endpoint: HttpEndpoint }): SingleEndpointSnippet | undefined { + this.assertHasPagination(endpoint); + const endpointSnippets = this.context.snippetGenerator.getSnippetsForEndpoint(endpoint.id); + const snippet = endpointSnippets?.userSpecified[0] ?? endpointSnippets?.autogenerated[0]; + return snippet; + } + + public generateHttpMethod({ + serviceId, + endpoint, + rawClientReference, + rawClient + }: { + serviceId: ServiceId; + endpoint: HttpEndpoint; + rawClientReference: string; + rawClient: RawClient; }): csharp.Method { - const endpointSignatureInfo = this.getEndpointSignatureInfo({ serviceId, endpoint }); + const endpointSignatureInfo = this.getUnpagedEndpointSignatureInfo({ serviceId, endpoint }); const parameters = [...endpointSignatureInfo.baseParameters]; - if (endpoint.idempotent) { - parameters.push( - csharp.parameter({ - type: csharp.Type.optional( - csharp.Type.reference(this.context.getIdempotentRequestOptionsClassReference()) - ), - name: this.context.getRequestOptionsParameterName(), - initializer: "null" - }) - ); - } else { - parameters.push( - csharp.parameter({ - type: csharp.Type.optional(csharp.Type.reference(this.context.getRequestOptionsClassReference())), - name: this.context.getIdempotentRequestOptionsParameterName(), - initializer: "null" - }) - ); - } + parameters.push(this.getRequestOptionsParameter({ endpoint })); parameters.push( csharp.parameter({ type: csharp.Type.reference(this.context.getCancellationTokenClassReference()), @@ -65,11 +94,10 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { }) ); const return_ = getEndpointReturnType({ context: this.context, endpoint }); - const endpointSnippets = this.context.snippetGenerator.getSnippetsForEndpoint(endpoint.id); - const snippet = endpointSnippets?.userSpecified[0] ?? endpointSnippets?.autogenerated[0]; + const snippet = this.getHttpMethodSnippet({ endpoint }); return csharp.method({ name: this.context.getEndpointMethodName(endpoint), - access: csharp.Access.Public, + access: this.hasPagination(endpoint) ? csharp.Access.Internal : csharp.Access.Public, isAsync: true, parameters, summary: endpoint.docs, @@ -110,34 +138,6 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { }); } - public generateHttpEndpointSnippet({ - example, - endpoint, - clientVariableName, - serviceId, - requestOptions, - getResult, - parseDatetimes - }: { - example: ExampleEndpointCall; - endpoint: HttpEndpoint; - clientVariableName: string; - serviceId: ServiceId; - requestOptions?: csharp.CodeBlock; - getResult?: boolean; - parseDatetimes: boolean; - }): csharp.MethodInvocation | undefined { - return this.generateEndpointSnippet({ - example, - endpoint, - clientVariableName, - serviceId, - additionalEndParameters: requestOptions != null ? [requestOptions] : [], - getResult, - parseDatetimes - }); - } - private getBaseURLForEndpoint({ endpoint }: { endpoint: HttpEndpoint }): csharp.CodeBlock { if (endpoint.baseUrl != null && this.context.ir.environments?.environments.type === "multipleBaseUrls") { const baseUrl = this.context.ir.environments?.environments.baseUrls.find( @@ -312,4 +312,401 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { }); }); } + + public generateHttpPagerMethod({ + serviceId, + endpoint + }: { + serviceId: ServiceId; + endpoint: HttpEndpoint; + }): csharp.Method { + this.assertHasPagination(endpoint); + const endpointSignatureInfo = this.getEndpointSignatureInfo({ serviceId, endpoint }); + const parameters = [...endpointSignatureInfo.baseParameters]; + const optionsParamName = this.getRequestOptionsParamNameForEndpoint({ endpoint }); + const requestOptionsParam = this.getRequestOptionsParameter({ endpoint }); + const requestOptionsType = requestOptionsParam.type; + parameters.push(requestOptionsParam); + const itemType = this.getPaginationItemType(endpoint); + const return_ = this.getPagerReturnType(endpoint); + const snippet = this.getHttpPagerMethodSnippet({ endpoint }); + return csharp.method({ + name: this.context.getEndpointMethodName(endpoint), + access: csharp.Access.Public, + isAsync: false, + parameters, + summary: endpoint.docs, + return_, + body: csharp.codeblock((writer) => { + const requestParam = endpointSignatureInfo.requestParameter; + if (!requestParam) { + throw new Error("Request parameter is required for pagination"); + } + const unpagedEndpointMethodName = this.context.getEndpointMethodName(endpoint); + const unpagedEndpointResponseType = getEndpointReturnType({ context: this.context, endpoint }); + if (!unpagedEndpointResponseType) { + throw new Error("Internal error; a response type is required for pagination endpoints"); + } + + writer.writeLine("if (request is not null)"); + writer.writeLine("{"); + writer.indent(); + writer.writeLine("request = request with { };"); + writer.dedent(); + writer.writeLine("}"); + + switch (endpoint.pagination.type) { + case "offset": + this.generateOffsetMethodBody({ + pagination: endpoint.pagination, + requestParam, + requestOptionsType, + unpagedEndpointResponseType, + itemType, + writer, + optionsParamName, + unpagedEndpointMethodName + }); + break; + case "cursor": + this.generateCursorMethodBody({ + pagination: endpoint.pagination, + requestParam, + requestOptionsType, + unpagedEndpointResponseType, + itemType, + writer, + optionsParamName, + unpagedEndpointMethodName + }); + break; + default: + assertNever(endpoint.pagination); + } + }), + codeExample: snippet?.endpointCall + }); + } + + private generateOffsetMethodBody({ + pagination, + requestParam, + requestOptionsType, + unpagedEndpointResponseType, + itemType, + writer, + optionsParamName, + unpagedEndpointMethodName + }: { + pagination: OffsetPagination; + requestParam: csharp.Parameter; + requestOptionsType: csharp.Type; + unpagedEndpointResponseType: csharp.Type; + itemType: csharp.Type; + writer: csharp.Writer; + optionsParamName: string; + unpagedEndpointMethodName: string; + }) { + const offsetType = this.context.csharpTypeMapper.convert({ + reference: pagination.page.property.valueType + }); + // use specified type or fallback to object + const stepType = pagination.step + ? this.context.csharpTypeMapper.convert({ + reference: pagination.step?.property.valueType + }) + : csharp.Type.object(); + const offsetPagerClassReference = this.context.getOffsetPagerClassReference({ + requestType: requestParam.type, + requestOptionsType, + responseType: unpagedEndpointResponseType, + offsetType, + stepType, + itemType + }); + writer.write("var pager = "); + writer.writeNodeStatement( + csharp.instantiateClass({ + classReference: offsetPagerClassReference, + arguments_: [ + csharp.codeblock(requestParam.name), + csharp.codeblock(optionsParamName), + csharp.codeblock(unpagedEndpointMethodName), + csharp.codeblock(`request => ${this.nullableDotGet("request", pagination.page)} ?? 0`), + csharp.codeblock((writer) => { + writer.writeLine("(request, offset) => {"); + writer.indent(); + this.initializeNestedObjects(writer, "request", pagination.page); + writer.writeTextStatement(`${this.dotGet("request", pagination.page)} = offset`); + writer.dedent(); + writer.writeLine("}"); + }), + csharp.codeblock( + pagination.step ? `request => ${this.nullableDotGet("request", pagination.step)} ?? 0` : "null" + ), + csharp.codeblock(`response => ${this.nullableDotGet("response", pagination.results)}?.ToList()`), + csharp.codeblock( + pagination.hasNextPage + ? `response => ${this.nullableDotGet("response", pagination.hasNextPage)}` + : "null" + ) + ] + }) + ); + writer.writeTextStatement("return pager"); + } + + private generateCursorMethodBody({ + pagination, + requestParam, + requestOptionsType, + unpagedEndpointResponseType, + itemType, + writer, + optionsParamName, + unpagedEndpointMethodName + }: { + pagination: CursorPagination; + requestParam: csharp.Parameter; + requestOptionsType: csharp.Type; + unpagedEndpointResponseType: csharp.Type; + itemType: csharp.Type; + writer: csharp.Writer; + optionsParamName: string; + unpagedEndpointMethodName: string; + }) { + const cursorType = this.context.csharpTypeMapper.convert({ + reference: pagination.next.property.valueType + }); + + const cursorPagerClassReference = this.context.getCursorPagerClassReference({ + requestType: requestParam.type, + requestOptionsType, + responseType: unpagedEndpointResponseType, + cursorType, + itemType + }); + writer.write("var pager = "); + writer.writeNodeStatement( + csharp.instantiateClass({ + classReference: cursorPagerClassReference, + arguments_: [ + csharp.codeblock(requestParam.name), + csharp.codeblock(optionsParamName), + csharp.codeblock(unpagedEndpointMethodName), + csharp.codeblock((writer) => { + writer.writeLine("(request, cursor) => {"); + writer.indent(); + this.initializeNestedObjects(writer, "request", pagination.page); + writer.writeTextStatement(`${this.dotGet("request", pagination.page)} = cursor`); + writer.dedent(); + writer.writeLine("}"); + }), + csharp.codeblock(`response => ${this.nullableDotGet("response", pagination.next)}`), + csharp.codeblock(`response => ${this.nullableDotGet("response", pagination.results)}?.ToList()`) + ] + }) + ); + writer.writeTextStatement("return pager"); + } + + private initializeNestedObjects(writer: csharp.Writer, variableName: string, { propertyPath }: RequestProperty) { + if (!propertyPath || propertyPath.length === 0) { + return; + } + + for (let i = 0; i < propertyPath.length; i++) { + const propertyPathPart = propertyPath.slice(0, i + 1); + writer.writeTextStatement( + `${variableName}.${propertyPathPart.map((val) => val.pascalCase.safeName).join(".")} ??= new ()` + ); + } + } + + private dotGet(variableName: string, { property, propertyPath }: RequestProperty | ResponseProperty): string { + if (!propertyPath || propertyPath.length === 0) { + return `${variableName}.${property.name.name.pascalCase.safeName}`; + } + return `${variableName}.${propertyPath.map((val) => val.pascalCase.safeName).join(".")}.${ + property.name.name.pascalCase.safeName + }`; + } + + private nullableDotGet( + variableName: string, + { property, propertyPath }: RequestProperty | ResponseProperty + ): string { + if (!propertyPath || propertyPath.length === 0) { + return `${variableName}?.${property.name.name.pascalCase.safeName}`; + } + + return `${variableName}?.${propertyPath.map((val) => val.pascalCase.safeName).join("?.")}?.${ + property.name.name.pascalCase.safeName + }`; + } + + public generateEndpointSnippet({ + example, + endpoint, + clientVariableName, + serviceId, + requestOptions, + getResult, + parseDatetimes + }: { + example: ExampleEndpointCall; + endpoint: HttpEndpoint; + clientVariableName: string; + serviceId: ServiceId; + requestOptions?: csharp.CodeBlock; + getResult?: boolean; + parseDatetimes: boolean; + }): csharp.MethodInvocation | undefined { + const additionalEndParameters = requestOptions != null ? [requestOptions] : []; + return this.hasPagination(endpoint) + ? this.generateHttpPagerEndpointSnippet({ + example, + endpoint, + clientVariableName, + serviceId, + additionalEndParameters, + getResult, + parseDatetimes + }) + : this.generateHttpEndpointSnippet({ + example, + endpoint, + clientVariableName, + serviceId, + additionalEndParameters, + getResult, + parseDatetimes + }); + } + + public generateHttpPagerEndpointSnippet({ + example, + endpoint, + clientVariableName, + serviceId, + additionalEndParameters, + getResult, + parseDatetimes + }: { + example: ExampleEndpointCall; + endpoint: HttpEndpoint; + clientVariableName: string; + serviceId: ServiceId; + additionalEndParameters?: csharp.CodeBlock[]; + getResult?: boolean; + parseDatetimes: boolean; + }): csharp.MethodInvocation | undefined { + return this.generatePagerEndpointSnippet({ + example, + endpoint, + clientVariableName, + serviceId, + additionalEndParameters, + getResult, + parseDatetimes + }); + } + + public generateHttpEndpointSnippet({ + example, + endpoint, + clientVariableName, + serviceId, + additionalEndParameters, + getResult, + parseDatetimes + }: { + example: ExampleEndpointCall; + endpoint: HttpEndpoint; + clientVariableName: string; + serviceId: ServiceId; + additionalEndParameters?: csharp.CodeBlock[]; + getResult?: boolean; + parseDatetimes: boolean; + }): csharp.MethodInvocation | undefined { + return super.generateEndpointSnippet({ + example, + endpoint, + clientVariableName, + serviceId, + additionalEndParameters, + getResult, + parseDatetimes + }); + } + + private generatePagerEndpointSnippet({ + example, + endpoint, + clientVariableName, + parseDatetimes, + serviceId, + additionalEndParameters + }: { + example: ExampleEndpointCall; + endpoint: HttpEndpoint; + clientVariableName: string; + serviceId: ServiceId; + parseDatetimes: boolean; + additionalEndParameters?: csharp.CodeBlock[]; + getResult?: boolean; + }): csharp.MethodInvocation | undefined { + const service = this.context.getHttpServiceOrThrow(serviceId); + const serviceFilePath = service.name.fernFilepath; + const args = this.getNonEndpointArguments(example, parseDatetimes); + const endpointRequestSnippet = this.getEndpointRequestSnippet(example, endpoint, serviceId, parseDatetimes); + if (endpointRequestSnippet != null) { + args.push(endpointRequestSnippet); + } + const on = csharp.codeblock((writer) => { + writer.write(`${clientVariableName}`); + for (const path of serviceFilePath.allParts) { + writer.write(`.${path.pascalCase.safeName}`); + } + }); + for (const endParameter of additionalEndParameters ?? []) { + args.push(endParameter); + } + + getEndpointReturnType({ context: this.context, endpoint }); + return new csharp.MethodInvocation({ + method: this.context.getEndpointMethodName(endpoint), + arguments_: args, + on, + async: false, + generics: [] + }); + } + + private getRequestOptionsParameter({ endpoint }: { endpoint: HttpEndpoint }): csharp.Parameter { + const name = this.getRequestOptionsParamNameForEndpoint({ endpoint }); + if (endpoint.idempotent) { + return csharp.parameter({ + type: csharp.Type.optional( + csharp.Type.reference(this.context.getIdempotentRequestOptionsClassReference()) + ), + name, + initializer: "null" + }); + } else { + return csharp.parameter({ + type: csharp.Type.optional(csharp.Type.reference(this.context.getRequestOptionsClassReference())), + name, + initializer: "null" + }); + } + } + + private getRequestOptionsParamNameForEndpoint({ endpoint }: { endpoint: HttpEndpoint }): string { + if (endpoint.idempotent) { + return this.context.getIdempotentRequestOptionsParameterName(); + } else { + return this.context.getRequestOptionsParameterName(); + } + } } diff --git a/generators/csharp/sdk/src/endpoint/snippets/EndpointSnippetsGenerator.ts b/generators/csharp/sdk/src/endpoint/snippets/EndpointSnippetsGenerator.ts index 2bf4d908b8c..0295001a70b 100644 --- a/generators/csharp/sdk/src/endpoint/snippets/EndpointSnippetsGenerator.ts +++ b/generators/csharp/sdk/src/endpoint/snippets/EndpointSnippetsGenerator.ts @@ -36,28 +36,36 @@ export class EndpointSnippetsGenerator { const endpointSnippetsById = new Map(); for (const [serviceId, service] of Object.entries(this.context.ir.services)) { for (const endpoint of service.endpoints) { - const autogenerated = endpoint.autogeneratedExamples - .map((example) => - this.generateSingleEndpointSnippet({ - endpoint, - example: example.example, - serviceId - }) - ) - .filter((snippet): snippet is SingleEndpointSnippet => snippet != null); - const userSpecified = endpoint.userSpecifiedExamples - .map((example) => - example.example != null - ? this.generateSingleEndpointSnippet({ - endpoint, - example: example.example, - serviceId - }) - : undefined - ) - .filter((snippet): snippet is SingleEndpointSnippet => snippet != null); - if (autogenerated.length > 0 || userSpecified.length > 0) { - endpointSnippetsById.set(endpoint.id, { autogenerated, userSpecified }); + const endpointIdsWithGenerator = [{ id: endpoint.id, isPager: false }]; + if (endpoint.pagination) { + endpointIdsWithGenerator.push({ id: endpoint.id, isPager: true }); + } + for (const { id, isPager } of endpointIdsWithGenerator) { + const autogenerated = endpoint.autogeneratedExamples + .flatMap((example) => + this.generateSingleEndpointSnippet({ + endpoint, + example: example.example, + serviceId, + generatePagerSnippet: isPager + }) + ) + .filter((snippet): snippet is SingleEndpointSnippet => snippet != null); + const userSpecified = endpoint.userSpecifiedExamples + .flatMap((example) => + example.example != null + ? this.generateSingleEndpointSnippet({ + endpoint, + example: example.example, + serviceId, + generatePagerSnippet: isPager + }) + : undefined + ) + .filter((snippet): snippet is SingleEndpointSnippet => snippet != null); + if (autogenerated.length > 0 || userSpecified.length > 0) { + endpointSnippetsById.set(id, { autogenerated, userSpecified }); + } } } } @@ -72,6 +80,7 @@ export class EndpointSnippetsGenerator { endpoint: HttpEndpoint; example: ExampleEndpointCall; serviceId: string; + generatePagerSnippet?: boolean; }): SingleEndpointSnippet | undefined { const isGrpc = this.context.getGrpcClientInfoForServiceId(serviceId); const methodInvocation = isGrpc diff --git a/generators/csharp/sdk/src/endpoint/snippets/SnippetJsonGenerator.ts b/generators/csharp/sdk/src/endpoint/snippets/SnippetJsonGenerator.ts index 28f5420587f..4bffa42f182 100644 --- a/generators/csharp/sdk/src/endpoint/snippets/SnippetJsonGenerator.ts +++ b/generators/csharp/sdk/src/endpoint/snippets/SnippetJsonGenerator.ts @@ -1,9 +1,7 @@ import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk"; import { Endpoint } from "@fern-fern/generator-exec-sdk/api"; -import { ExampleEndpointCall, HttpEndpoint } from "@fern-fern/ir-sdk/api"; +import { HttpEndpoint } from "@fern-fern/ir-sdk/api"; import { SdkGeneratorContext } from "../../SdkGeneratorContext"; -import { GrpcEndpointGenerator } from "../grpc/GrpcEndpointGenerator"; -import { HttpEndpointGenerator } from "../http/HttpEndpointGenerator"; import urlJoin from "url-join"; import { RootClientGenerator } from "../../root-client/RootClientGenerator"; import { SingleEndpointSnippet } from "./EndpointSnippetsGenerator"; diff --git a/generators/csharp/sdk/src/reference/buildReference.ts b/generators/csharp/sdk/src/reference/buildReference.ts index 25bc6d8cd5e..3ab6b5c5621 100644 --- a/generators/csharp/sdk/src/reference/buildReference.ts +++ b/generators/csharp/sdk/src/reference/buildReference.ts @@ -36,23 +36,22 @@ function getEndpointReferencesForService({ endpoint, example: context.getExampleEndpointCallOrThrow(endpoint) }); - if (singleEndpointSnippet == null) { - continue; - } - const endpointSignatureInfo = context.endpointGenerator.getEndpointSignatureInfo({ - serviceId, - endpoint - }); - result.push( - getEndpointReference({ - context, + if (singleEndpointSnippet != null) { + const endpointSignatureInfo = context.endpointGenerator.getEndpointSignatureInfo({ serviceId, - service, - endpoint, - endpointSignatureInfo, - singleEndpointSnippet - }) - ); + endpoint + }); + result.push( + getEndpointReference({ + context, + serviceId, + service, + endpoint, + endpointSignatureInfo, + singleEndpointSnippet + }) + ); + } } return result; } @@ -63,7 +62,8 @@ function getEndpointReference({ service, endpoint, endpointSignatureInfo, - singleEndpointSnippet + singleEndpointSnippet, + isPager = false }: { context: SdkGeneratorContext; serviceId: ServiceId; @@ -71,6 +71,7 @@ function getEndpointReference({ endpoint: HttpEndpoint; endpointSignatureInfo: EndpointSignatureInfo; singleEndpointSnippet: SingleEndpointSnippet; + isPager?: boolean; }): FernGeneratorCli.EndpointReference { return { title: { diff --git a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts index f2f554f8995..734c54fdaa1 100644 --- a/generators/csharp/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/csharp/sdk/src/root-client/RootClientGenerator.ts @@ -116,7 +116,7 @@ export class RootClientGenerator extends FileGenerator "); - } - writer.writeNode(endpointSnippet); - if (!responseSupported) { - writer.write(")"); - } - writer.write(";"); + if (this.endpoint.pagination) { + writer.write("var pager = "); + writer.writeNode(endpointSnippet); + writer.write(";"); + writer.newLine(); + writer.write("await foreach (var item in pager)"); + writer.newLine(); + writer.write("{"); + writer.newLine(); + writer.indent(); - if (responseSupported) { + writer.writeTextStatement("Assert.That(item, Is.Not.Null)"); + writer.write("break; // Only check the first item"); + + writer.dedent(); writer.newLine(); - if (responseBodyType === "json") { - writer.addReference(this.context.getFluentAssetionsJsonClassReference()); - writer.writeNode(this.context.getJTokenClassReference()); - writer.write(".Parse(mockResponse).Should().BeEquivalentTo("); - writer.writeNode(this.context.getJTokenClassReference()); - writer.write(".Parse("); - writer.writeNode(this.context.getJsonUtilsClassReference()); - writer.writeTextStatement(".Serialize(response)))"); - } else if (responseBodyType === "text") { - writer.writeTextStatement("Assert.That(response, Is.EqualTo(mockResponse))"); + writer.write("}"); + } else { + if (responseSupported) { + writer.write("var response = "); + writer.writeNode(endpointSnippet); + writer.write(";"); + writer.newLine(); + if (responseBodyType === "json") { + writer.addReference(this.context.getFluentAssetionsJsonClassReference()); + writer.writeNode(this.context.getJTokenClassReference()); + writer.write(".Parse(mockResponse).Should().BeEquivalentTo("); + writer.writeNode(this.context.getJTokenClassReference()); + writer.write(".Parse("); + writer.writeNode(this.context.getJsonUtilsClassReference()); + writer.writeTextStatement(".Serialize(response)))"); + } else if (responseBodyType === "text") { + writer.writeTextStatement("Assert.That(response, Is.EqualTo(mockResponse))"); + } + } else { + writer.write("Assert.DoesNotThrowAsync(async () => "); + writer.writeNode(endpointSnippet); + writer.write(");"); } } }); diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index 357df6b2e3f..93995ca38f6 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -6,6 +6,16 @@ # The C# SDK now uses forward-compatible enums which are not compatible with the previously generated enums. # Set `enable-forward-compatible-enums` to `false` in the configuration to generate the old enums. # irVersion: 53 +- version: 1.9.9 + createdAt: "2024-11-19" + changelogEntry: + - type: feat + summary: | + Add support for [Auto Pagination](https://buildwithfern.com/learn/sdks/features/auto-pagination). + When enabled, the endpoint methods will return a `Pager` object that you can use to iterate over all items of an endpoint. + Additionally, you can use the `Pager.AsPagesAsync` method to iterate over all pages of an endpoint. + The SDK will automatically make the necessary HTTP requests for you as you iterate over the items or the pages. + irVersion: 53 - version: 1.9.8 createdAt: "2024-11-14" changelogEntry: diff --git a/package.json b/package.json index 7570f36fa2d..6e5db0665ce 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "lint:eslint:fix": "pnpm lint:eslint --fix", "lint:style": "stylelint 'packages/**/src/**/*.scss' --allow-empty-input --max-warnings 0", "lint:style:fix": "pnpm lint:style --fix", - "format": "prettier --write --ignore-unknown --ignore-path ./shared/.prettierignore", + "format": "prettier --write --ignore-unknown --ignore-path ./shared/.prettierignore \"**\"", "format:fix": "pnpm format --ignore-path ./shared/.prettierignore \"**\"", "format:check": "prettier --check --ignore-unknown --ignore-path ./shared/.prettierignore \"**\"", "add-workspace": "yarn mrlint add-workspace", diff --git a/seed/csharp-model/pagination/.mock/definition/users.yml b/seed/csharp-model/pagination/.mock/definition/users.yml index ecde10ef707..ddc0bc1848b 100644 --- a/seed/csharp-model/pagination/.mock/definition/users.yml +++ b/seed/csharp-model/pagination/.mock/definition/users.yml @@ -1,9 +1,9 @@ imports: root: __package__.yml -types: - Order: - enum: +types: + Order: + enum: - asc - desc @@ -15,8 +15,8 @@ types: properties: cursor: optional - UserListContainer: - properties: + UserListContainer: + properties: users: list UserPage: @@ -24,60 +24,59 @@ types: data: UserListContainer next: optional - UserOptionalListContainer: - properties: + UserOptionalListContainer: + properties: users: optional> UserOptionalListPage: properties: data: UserOptionalListContainer next: optional - UsernameContainer: properties: results: list - ListUsersExtendedResponse: + ListUsersExtendedResponse: extends: - UserPage properties: - total_count: + total_count: type: integer docs: The totall number of /users - ListUsersExtendedOptionalListResponse: + ListUsersExtendedOptionalListResponse: extends: - UserOptionalListPage properties: - total_count: + total_count: type: integer docs: The totall number of /users - - ListUsersPaginationResponse: - properties: + + ListUsersPaginationResponse: + properties: hasNextPage: optional page: optional - total_count: + total_count: type: integer docs: The totall number of /users data: list - Page: - properties: - page: + Page: + properties: + page: type: integer docs: The current page next: optional per_page: integer total_page: integer - NextPage: - properties: + NextPage: + properties: page: integer starting_after: string - User: - properties: + User: + properties: name: string id: integer @@ -86,7 +85,7 @@ service: base-path: /users endpoints: listWithCursorPagination: - pagination: + pagination: cursor: $request.starting_after next_cursor: $response.page.next.starting_after results: $response.data @@ -95,41 +94,41 @@ service: request: name: ListUsersCursorPaginationRequest query-parameters: - page: + page: type: optional docs: Defaults to first page - per_page: + per_page: type: optional docs: Defaults to per page - order: - type: optional - starting_after: + order: + type: optional + starting_after: type: optional - docs: | - The cursor used for pagination in order to fetch + docs: | + The cursor used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse listWithBodyCursorPagination: - pagination: + pagination: cursor: $request.pagination.cursor next_cursor: $response.page.next.starting_after results: $response.data method: POST path: "" - request: + request: name: ListUsersBodyCursorPaginationRequest - body: + body: properties: - pagination: + pagination: type: optional - docs: | + docs: | The object that contains the cursor used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse listWithOffsetPagination: - pagination: + pagination: offset: $request.page results: $response.data method: GET @@ -137,38 +136,37 @@ service: request: name: ListUsersOffsetPaginationRequest query-parameters: - page: + page: type: optional docs: Defaults to first page - per_page: + per_page: type: optional docs: Defaults to per page - order: - type: optional - starting_after: + order: + type: optional + starting_after: type: optional - docs: | - The cursor used for pagination in order to fetch + docs: | + The cursor used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse listWithBodyOffsetPagination: - pagination: + pagination: offset: $request.pagination.page results: $response.data method: POST path: "" - request: + request: name: ListUsersBodyOffsetPaginationRequest - body: + body: properties: - pagination: + pagination: type: optional - docs: | + docs: | The object that contains the offset used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse - listWithOffsetStepPagination: pagination: offset: $request.page @@ -191,8 +189,8 @@ service: order: type: optional response: ListUsersPaginationResponse - - listWithOffsetPaginationHasNextPage: + + listWithOffsetPaginationHasNextPage: pagination: offset: $request.page results: $response.data @@ -217,7 +215,7 @@ service: response: ListUsersPaginationResponse listWithExtendedResults: - pagination: + pagination: cursor: $request.cursor next_cursor: $response.next results: $response.data.users @@ -230,7 +228,7 @@ service: response: ListUsersExtendedResponse listWithExtendedResultsAndOptionalData: - pagination: + pagination: cursor: $request.cursor next_cursor: $response.next results: $response.data.users @@ -243,7 +241,7 @@ service: response: ListUsersExtendedOptionalListResponse listUsernames: - pagination: + pagination: cursor: $request.starting_after next_cursor: $response.cursor.after results: $response.cursor.data @@ -252,10 +250,10 @@ service: request: name: ListUsernamesRequest query-parameters: - starting_after: + starting_after: type: optional - docs: | - The cursor used for pagination in order to fetch + docs: | + The cursor used for pagination in order to fetch the next page of results. response: root.UsernameCursor @@ -266,6 +264,6 @@ service: request: name: ListWithGlobalConfigRequest query-parameters: - offset: + offset: type: optional response: UsernameContainer \ No newline at end of file diff --git a/seed/csharp-sdk/pagination/.mock/definition/users.yml b/seed/csharp-sdk/pagination/.mock/definition/users.yml index ecde10ef707..ddc0bc1848b 100644 --- a/seed/csharp-sdk/pagination/.mock/definition/users.yml +++ b/seed/csharp-sdk/pagination/.mock/definition/users.yml @@ -1,9 +1,9 @@ imports: root: __package__.yml -types: - Order: - enum: +types: + Order: + enum: - asc - desc @@ -15,8 +15,8 @@ types: properties: cursor: optional - UserListContainer: - properties: + UserListContainer: + properties: users: list UserPage: @@ -24,60 +24,59 @@ types: data: UserListContainer next: optional - UserOptionalListContainer: - properties: + UserOptionalListContainer: + properties: users: optional> UserOptionalListPage: properties: data: UserOptionalListContainer next: optional - UsernameContainer: properties: results: list - ListUsersExtendedResponse: + ListUsersExtendedResponse: extends: - UserPage properties: - total_count: + total_count: type: integer docs: The totall number of /users - ListUsersExtendedOptionalListResponse: + ListUsersExtendedOptionalListResponse: extends: - UserOptionalListPage properties: - total_count: + total_count: type: integer docs: The totall number of /users - - ListUsersPaginationResponse: - properties: + + ListUsersPaginationResponse: + properties: hasNextPage: optional page: optional - total_count: + total_count: type: integer docs: The totall number of /users data: list - Page: - properties: - page: + Page: + properties: + page: type: integer docs: The current page next: optional per_page: integer total_page: integer - NextPage: - properties: + NextPage: + properties: page: integer starting_after: string - User: - properties: + User: + properties: name: string id: integer @@ -86,7 +85,7 @@ service: base-path: /users endpoints: listWithCursorPagination: - pagination: + pagination: cursor: $request.starting_after next_cursor: $response.page.next.starting_after results: $response.data @@ -95,41 +94,41 @@ service: request: name: ListUsersCursorPaginationRequest query-parameters: - page: + page: type: optional docs: Defaults to first page - per_page: + per_page: type: optional docs: Defaults to per page - order: - type: optional - starting_after: + order: + type: optional + starting_after: type: optional - docs: | - The cursor used for pagination in order to fetch + docs: | + The cursor used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse listWithBodyCursorPagination: - pagination: + pagination: cursor: $request.pagination.cursor next_cursor: $response.page.next.starting_after results: $response.data method: POST path: "" - request: + request: name: ListUsersBodyCursorPaginationRequest - body: + body: properties: - pagination: + pagination: type: optional - docs: | + docs: | The object that contains the cursor used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse listWithOffsetPagination: - pagination: + pagination: offset: $request.page results: $response.data method: GET @@ -137,38 +136,37 @@ service: request: name: ListUsersOffsetPaginationRequest query-parameters: - page: + page: type: optional docs: Defaults to first page - per_page: + per_page: type: optional docs: Defaults to per page - order: - type: optional - starting_after: + order: + type: optional + starting_after: type: optional - docs: | - The cursor used for pagination in order to fetch + docs: | + The cursor used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse listWithBodyOffsetPagination: - pagination: + pagination: offset: $request.pagination.page results: $response.data method: POST path: "" - request: + request: name: ListUsersBodyOffsetPaginationRequest - body: + body: properties: - pagination: + pagination: type: optional - docs: | + docs: | The object that contains the offset used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse - listWithOffsetStepPagination: pagination: offset: $request.page @@ -191,8 +189,8 @@ service: order: type: optional response: ListUsersPaginationResponse - - listWithOffsetPaginationHasNextPage: + + listWithOffsetPaginationHasNextPage: pagination: offset: $request.page results: $response.data @@ -217,7 +215,7 @@ service: response: ListUsersPaginationResponse listWithExtendedResults: - pagination: + pagination: cursor: $request.cursor next_cursor: $response.next results: $response.data.users @@ -230,7 +228,7 @@ service: response: ListUsersExtendedResponse listWithExtendedResultsAndOptionalData: - pagination: + pagination: cursor: $request.cursor next_cursor: $response.next results: $response.data.users @@ -243,7 +241,7 @@ service: response: ListUsersExtendedOptionalListResponse listUsernames: - pagination: + pagination: cursor: $request.starting_after next_cursor: $response.cursor.after results: $response.cursor.data @@ -252,10 +250,10 @@ service: request: name: ListUsernamesRequest query-parameters: - starting_after: + starting_after: type: optional - docs: | - The cursor used for pagination in order to fetch + docs: | + The cursor used for pagination in order to fetch the next page of results. response: root.UsernameCursor @@ -266,6 +264,6 @@ service: request: name: ListWithGlobalConfigRequest query-parameters: - offset: + offset: type: optional response: UsernameContainer \ No newline at end of file diff --git a/seed/csharp-sdk/pagination/reference.md b/seed/csharp-sdk/pagination/reference.md index a58df02639d..8d50cf5f03a 100644 --- a/seed/csharp-sdk/pagination/reference.md +++ b/seed/csharp-sdk/pagination/reference.md @@ -1,6 +1,6 @@ # Reference ## Users -
client.Users.ListWithCursorPaginationAsync(ListUsersCursorPaginationRequest { ... }) -> ListUsersPaginationResponse +
client.Users.ListWithCursorPaginationAsync(ListUsersCursorPaginationRequest { ... }) -> Pager
@@ -48,7 +48,7 @@ await client.Users.ListWithCursorPaginationAsync(
-
client.Users.ListWithBodyCursorPaginationAsync(ListUsersBodyCursorPaginationRequest { ... }) -> ListUsersPaginationResponse +
client.Users.ListWithBodyCursorPaginationAsync(ListUsersBodyCursorPaginationRequest { ... }) -> Pager
@@ -90,7 +90,7 @@ await client.Users.ListWithBodyCursorPaginationAsync(
-
client.Users.ListWithOffsetPaginationAsync(ListUsersOffsetPaginationRequest { ... }) -> ListUsersPaginationResponse +
client.Users.ListWithOffsetPaginationAsync(ListUsersOffsetPaginationRequest { ... }) -> Pager
@@ -138,7 +138,7 @@ await client.Users.ListWithOffsetPaginationAsync(
-
client.Users.ListWithBodyOffsetPaginationAsync(ListUsersBodyOffsetPaginationRequest { ... }) -> ListUsersPaginationResponse +
client.Users.ListWithBodyOffsetPaginationAsync(ListUsersBodyOffsetPaginationRequest { ... }) -> Pager
@@ -180,7 +180,7 @@ await client.Users.ListWithBodyOffsetPaginationAsync(
-
client.Users.ListWithOffsetStepPaginationAsync(ListUsersOffsetStepPaginationRequest { ... }) -> ListUsersPaginationResponse +
client.Users.ListWithOffsetStepPaginationAsync(ListUsersOffsetStepPaginationRequest { ... }) -> Pager
@@ -227,7 +227,7 @@ await client.Users.ListWithOffsetStepPaginationAsync(
-
client.Users.ListWithOffsetPaginationHasNextPageAsync(ListWithOffsetPaginationHasNextPageRequest { ... }) -> ListUsersPaginationResponse +
client.Users.ListWithOffsetPaginationHasNextPageAsync(ListWithOffsetPaginationHasNextPageRequest { ... }) -> Pager
@@ -274,7 +274,7 @@ await client.Users.ListWithOffsetPaginationHasNextPageAsync(
-
client.Users.ListWithExtendedResultsAsync(ListUsersExtendedRequest { ... }) -> ListUsersExtendedResponse +
client.Users.ListWithExtendedResultsAsync(ListUsersExtendedRequest { ... }) -> Pager
@@ -316,7 +316,7 @@ await client.Users.ListWithExtendedResultsAsync(
-
client.Users.ListWithExtendedResultsAndOptionalDataAsync(ListUsersExtendedRequestForOptionalData { ... }) -> ListUsersExtendedOptionalListResponse +
client.Users.ListWithExtendedResultsAndOptionalDataAsync(ListUsersExtendedRequestForOptionalData { ... }) -> Pager
@@ -358,7 +358,7 @@ await client.Users.ListWithExtendedResultsAndOptionalDataAsync(
-
client.Users.ListUsernamesAsync(ListUsernamesRequest { ... }) -> UsernameCursor +
client.Users.ListUsernamesAsync(ListUsernamesRequest { ... }) -> Pager
@@ -400,7 +400,7 @@ await client.Users.ListUsernamesAsync(
-
client.Users.ListWithGlobalConfigAsync(ListWithGlobalConfigRequest { ... }) -> UsernameContainer +
client.Users.ListWithGlobalConfigAsync(ListWithGlobalConfigRequest { ... }) -> Pager
diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/GuidCursorTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/GuidCursorTest.cs new file mode 100644 index 00000000000..ea3be49edc8 --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/GuidCursorTest.cs @@ -0,0 +1,107 @@ +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class GuidCursorTest +{ + [Test] + public async Task CursorPagerShouldWorkWithGuidCursors() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private static readonly Guid? Cursor1 = null; + private static readonly Guid Cursor2 = new("00000000-0000-0000-0000-000000000001"); + private static readonly Guid Cursor3 = new("00000000-0000-0000-0000-000000000001"); + private Guid? _cursorCopy; + + private Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() { Items = ["item1", "item2"] }, + Cursor = new() { Next = Cursor2 }, + }, + new() + { + Data = new() { Items = ["item1"] }, + Cursor = new() { Next = Cursor3 }, + }, + new() + { + Data = new() { Items = [] }, + Cursor = new() { Next = null }, + }, + }.GetEnumerator(); + _cursorCopy = Cursor1; + Pager pager = new CursorPager( + new() { Cursor = Cursor1 }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + (request, cursor) => + { + request.Cursor = cursor; + _cursorCopy = cursor; + }, + response => response?.Cursor?.Next, + response => response?.Data?.Items?.ToList() + ); + return pager; + } + + private async Task AssertPager(Pager pager) + { + var pageEnumerator = pager.AsPagesAsync().GetAsyncEnumerator(); + + // first page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + var page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(2)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor1)); + + // second page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(1)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor2)); + + // third page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(0)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor3)); + + // no more + Assert.That(await pageEnumerator.MoveNextAsync(), Is.False); + } + + private class Request + { + public required Guid? Cursor { get; set; } + } + + private class Response + { + public required Data Data { get; set; } + public required Cursor Cursor { get; set; } + } + + private class Data + { + public required IEnumerable Items { get; set; } + } + + private class Cursor + { + public required Guid? Next { get; set; } + } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/HasNextPageOffsetTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/HasNextPageOffsetTest.cs new file mode 100644 index 00000000000..3d5dd8b018d --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/HasNextPageOffsetTest.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class HasNextPageOffsetTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithHasNextPage() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private static Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() { Items = ["item1", "item2"] }, + HasNext = true, + }, + new() + { + Data = new() { Items = ["item1", "item2"] }, + HasNext = true, + }, + new() + { + Data = new() { Items = ["item1"] }, + HasNext = false, + }, + }.GetEnumerator(); + Pager pager = new OffsetPager( + new() { Pagination = new() { Page = 1 } }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request?.Pagination?.Page ?? 0, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.Page = offset; + }, + null, + response => response?.Data?.Items?.ToList(), + response => response.HasNext + ); + return pager; + } + + private static async Task AssertPager(Pager pager) + { + var pageCounter = 0; + var itemCounter = 0; + await foreach (var page in pager.AsPagesAsync()) + { + pageCounter++; + itemCounter += page.Items.Count; + } + + Assert.Multiple(() => + { + Assert.That(pageCounter, Is.EqualTo(3)); + Assert.That(itemCounter, Is.EqualTo(5)); + }); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public int Page { get; set; } + } + + private class Response + { + public Data Data { get; set; } + public bool HasNext { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/IntOffsetTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/IntOffsetTest.cs new file mode 100644 index 00000000000..702d4535f04 --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/IntOffsetTest.cs @@ -0,0 +1,81 @@ +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class IntOffsetTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithIntPage() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + public Pager CreatePager() + { + var responses = new List + { + new() { Data = new() { Items = ["item1", "item2"] } }, + new() { Data = new() { Items = ["item1"] } }, + new() { Data = new() { Items = [] } }, + }.GetEnumerator(); + Pager pager = new OffsetPager( + new() { Pagination = new() { Page = 1 } }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request.Pagination.Page, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.Page = offset; + }, + null, + response => response?.Data?.Items?.ToList(), + null + ); + return pager; + } + + public async Task AssertPager(Pager pager) + { + var pageCounter = 0; + var itemCounter = 0; + await foreach (var page in pager.AsPagesAsync()) + { + pageCounter++; + itemCounter += page.Items.Count; + } + + Assert.Multiple(() => + { + Assert.That(pageCounter, Is.EqualTo(3)); + Assert.That(itemCounter, Is.EqualTo(3)); + }); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public int Page { get; set; } + } + + private class Response + { + public Data Data { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/LongOffsetTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/LongOffsetTest.cs new file mode 100644 index 00000000000..5db607599e6 --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/LongOffsetTest.cs @@ -0,0 +1,81 @@ +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class LongOffsetTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithLongPage() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private static Pager CreatePager() + { + var responses = new List + { + new() { Data = new() { Items = ["item1", "item2"] } }, + new() { Data = new() { Items = ["item1"] } }, + new() { Data = new() { Items = [] } }, + }.GetEnumerator(); + Pager pager = new OffsetPager( + new() { Pagination = new() { Page = 1 } }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request?.Pagination?.Page ?? 0, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.Page = offset; + }, + null, + response => response?.Data?.Items?.ToList(), + null + ); + return pager; + } + + private static async Task AssertPager(Pager pager) + { + var pageCounter = 0; + var itemCounter = 0; + await foreach (var page in pager.AsPagesAsync()) + { + pageCounter++; + itemCounter += page.Items.Count; + } + + Assert.Multiple(() => + { + Assert.That(pageCounter, Is.EqualTo(3)); + Assert.That(itemCounter, Is.EqualTo(3)); + }); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public long Page { get; set; } + } + + private class Response + { + public Data Data { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/NoRequestCursorTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/NoRequestCursorTest.cs new file mode 100644 index 00000000000..f7647fbb637 --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/NoRequestCursorTest.cs @@ -0,0 +1,107 @@ +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class NoRequestCursorTest +{ + [Test] + public async Task CursorPagerShouldWorkWithStringCursor() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private const string? Cursor1 = null; + private const string Cursor2 = "cursor2"; + private const string Cursor3 = "cursor3"; + private string? _cursorCopy; + + private Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() { Items = ["item1", "item2"] }, + Cursor = new() { Next = Cursor2 }, + }, + new() + { + Data = new() { Items = ["item1"] }, + Cursor = new() { Next = Cursor3 }, + }, + new() + { + Data = new() { Items = [] }, + Cursor = new() { Next = null }, + }, + }.GetEnumerator(); + _cursorCopy = Cursor1; + Pager pager = new CursorPager( + null, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + (request, cursor) => + { + request.Cursor = cursor; + _cursorCopy = cursor; + }, + response => response?.Cursor?.Next, + response => response?.Data?.Items?.ToList() + ); + return pager; + } + + private async Task AssertPager(Pager pager) + { + var pageEnumerator = pager.AsPagesAsync().GetAsyncEnumerator(); + + // first page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + var page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(2)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor1)); + + // second page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(1)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor2)); + + // third page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(0)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor3)); + + // no more + Assert.That(await pageEnumerator.MoveNextAsync(), Is.False); + } + + private class Request + { + public required string? Cursor { get; set; } + } + + private class Response + { + public required Data Data { get; set; } + public required Cursor Cursor { get; set; } + } + + private class Data + { + public required IEnumerable Items { get; set; } + } + + private class Cursor + { + public required string? Next { get; set; } + } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/NoRequestOffsetTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/NoRequestOffsetTest.cs new file mode 100644 index 00000000000..0cb2e953ba4 --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/NoRequestOffsetTest.cs @@ -0,0 +1,81 @@ +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class NoRequestOffsetTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithoutRequest() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + public Pager CreatePager() + { + var responses = new List + { + new() { Data = new() { Items = ["item1", "item2"] } }, + new() { Data = new() { Items = ["item1"] } }, + new() { Data = new() { Items = [] } }, + }.GetEnumerator(); + Pager pager = new OffsetPager( + null, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request?.Pagination?.Page ?? 0, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.Page = offset; + }, + null, + response => response?.Data?.Items?.ToList(), + null + ); + return pager; + } + + public async Task AssertPager(Pager pager) + { + var pageCounter = 0; + var itemCounter = 0; + await foreach (var page in pager.AsPagesAsync()) + { + pageCounter++; + itemCounter += page.Items.Count; + } + + Assert.Multiple(() => + { + Assert.That(pageCounter, Is.EqualTo(3)); + Assert.That(itemCounter, Is.EqualTo(3)); + }); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public int Page { get; set; } + } + + private class Response + { + public Data Data { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/StepOffsetTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/StepOffsetTest.cs new file mode 100644 index 00000000000..724231d4b5a --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/StepOffsetTest.cs @@ -0,0 +1,96 @@ +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class StepPageOffsetPaginationTest +{ + [Test] + public async Task OffsetPagerShouldWorkWithStep() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private Pagination _paginationCopy; + + private Pager CreatePager() + { + var responses = new List + { + new() { Data = new() { Items = ["item1", "item2"] } }, + new() { Data = new() { Items = ["item1"] } }, + new() { Data = new() { Items = [] } }, + }.GetEnumerator(); + _paginationCopy = new() { ItemOffset = 0, PageSize = 2 }; + Pager pager = new OffsetPager( + new() { Pagination = _paginationCopy }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + request => request?.Pagination?.ItemOffset ?? 0, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.ItemOffset = offset; + _paginationCopy = request.Pagination; + }, + request => request?.Pagination?.PageSize, + response => response?.Data?.Items?.ToList(), + null + ); + return pager; + } + + private async Task AssertPager(Pager pager) + { + var pageEnumerator = pager.AsPagesAsync().GetAsyncEnumerator(); + + // first page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + var page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(2)); + Assert.That(_paginationCopy.ItemOffset, Is.EqualTo(0)); + + // second page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(1)); + Assert.That(_paginationCopy.ItemOffset, Is.EqualTo(2)); + + // third page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(0)); + Assert.That(_paginationCopy.ItemOffset, Is.EqualTo(3)); + + // no more + Assert.That(await pageEnumerator.MoveNextAsync(), Is.False); + } + + private class Request + { + public Pagination Pagination { get; set; } + } + + private class Pagination + { + public int ItemOffset { get; set; } + public int PageSize { get; set; } + } + + private class Response + { + public Data Data { get; set; } + public bool HasNext { get; set; } + } + + private class Data + { + public IEnumerable Items { get; set; } + } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/StringCursorTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/StringCursorTest.cs new file mode 100644 index 00000000000..790de1f54c9 --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Core/Pagination/StringCursorTest.cs @@ -0,0 +1,107 @@ +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Pagination; + +[TestFixture(Category = "Pagination")] +public class StringCursorTest +{ + [Test] + public async Task CursorPagerShouldWorkWithStringCursor() + { + var pager = CreatePager(); + await AssertPager(pager); + } + + private const string? Cursor1 = null; + private const string Cursor2 = "cursor2"; + private const string Cursor3 = "cursor3"; + private string? _cursorCopy; + + private Pager CreatePager() + { + var responses = new List + { + new() + { + Data = new() { Items = ["item1", "item2"] }, + Cursor = new() { Next = Cursor2 }, + }, + new() + { + Data = new() { Items = ["item1"] }, + Cursor = new() { Next = Cursor3 }, + }, + new() + { + Data = new() { Items = [] }, + Cursor = new() { Next = null }, + }, + }.GetEnumerator(); + _cursorCopy = Cursor1; + Pager pager = new CursorPager( + new() { Cursor = Cursor1 }, + null, + (_, _, _) => + { + responses.MoveNext(); + return Task.FromResult(responses.Current); + }, + (request, cursor) => + { + request.Cursor = cursor; + _cursorCopy = cursor; + }, + response => response?.Cursor?.Next, + response => response?.Data?.Items?.ToList() + ); + return pager; + } + + private async Task AssertPager(Pager pager) + { + var pageEnumerator = pager.AsPagesAsync().GetAsyncEnumerator(); + + // first page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + var page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(2)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor1)); + + // second page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(1)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor2)); + + // third page + Assert.That(await pageEnumerator.MoveNextAsync(), Is.True); + page = pageEnumerator.Current; + Assert.That(page.Items, Has.Count.EqualTo(0)); + Assert.That(_cursorCopy, Is.EqualTo(Cursor3)); + + // no more + Assert.That(await pageEnumerator.MoveNextAsync(), Is.False); + } + + private class Request + { + public required string? Cursor { get; set; } + } + + private class Response + { + public required Data Data { get; set; } + public required Cursor Cursor { get; set; } + } + + private class Data + { + public required IEnumerable Items { get; set; } + } + + private class Cursor + { + public required string? Next { get; set; } + } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListUsernamesTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListUsernamesTest.cs index ae3186eece7..69aa6a29892 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListUsernamesTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListUsernamesTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -42,13 +39,14 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListUsernamesAsync( + var pager = Client.Users.ListUsernamesAsync( new ListUsernamesRequest { StartingAfter = "starting_after" }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithBodyCursorPaginationTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithBodyCursorPaginationTest.cs index 7ec736377ba..adc615139c9 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithBodyCursorPaginationTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithBodyCursorPaginationTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -64,16 +61,17 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListWithBodyCursorPaginationAsync( + var pager = Client.Users.ListWithBodyCursorPaginationAsync( new ListUsersBodyCursorPaginationRequest { Pagination = new WithCursor { Cursor = "cursor" }, }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithBodyOffsetPaginationTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithBodyOffsetPaginationTest.cs index d8e44127b48..0579f54fa0d 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithBodyOffsetPaginationTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithBodyOffsetPaginationTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -64,13 +61,14 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListWithBodyOffsetPaginationAsync( + var pager = Client.Users.ListWithBodyOffsetPaginationAsync( new ListUsersBodyOffsetPaginationRequest { Pagination = new WithPage { Page = 1 } }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithCursorPaginationTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithCursorPaginationTest.cs index 9d97ada8589..de6381a57c6 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithCursorPaginationTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithCursorPaginationTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -59,7 +56,7 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListWithCursorPaginationAsync( + var pager = Client.Users.ListWithCursorPaginationAsync( new ListUsersCursorPaginationRequest { Page = 1, @@ -69,9 +66,10 @@ public async Task MockServerTest() }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithExtendedResultsAndOptionalDataTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithExtendedResultsAndOptionalDataTest.cs index 74d0f4138cb..189f894fa61 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithExtendedResultsAndOptionalDataTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithExtendedResultsAndOptionalDataTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -36,16 +33,17 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListWithExtendedResultsAndOptionalDataAsync( + var pager = Client.Users.ListWithExtendedResultsAndOptionalDataAsync( new ListUsersExtendedRequestForOptionalData { Cursor = "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32", }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithExtendedResultsTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithExtendedResultsTest.cs index aaf400738a0..8d0124ce461 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithExtendedResultsTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithExtendedResultsTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -36,13 +33,14 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListWithExtendedResultsAsync( + var pager = Client.Users.ListWithExtendedResultsAsync( new ListUsersExtendedRequest { Cursor = "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithGlobalConfigTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithGlobalConfigTest.cs index 5a98621c0fa..a81dc0fbd2c 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithGlobalConfigTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithGlobalConfigTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -39,13 +36,14 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListWithGlobalConfigAsync( + var pager = Client.Users.ListWithGlobalConfigAsync( new ListWithGlobalConfigRequest { Offset = 1 }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetPaginationHasNextPageTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetPaginationHasNextPageTest.cs index d70397dbbdd..bb82f08e9b6 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetPaginationHasNextPageTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetPaginationHasNextPageTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -58,7 +55,7 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListWithOffsetPaginationHasNextPageAsync( + var pager = Client.Users.ListWithOffsetPaginationHasNextPageAsync( new ListWithOffsetPaginationHasNextPageRequest { Page = 1, @@ -67,9 +64,10 @@ public async Task MockServerTest() }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetPaginationTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetPaginationTest.cs index ae9cf557bdc..9f35ed795ca 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetPaginationTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetPaginationTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -59,7 +56,7 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListWithOffsetPaginationAsync( + var pager = Client.Users.ListWithOffsetPaginationAsync( new ListUsersOffsetPaginationRequest { Page = 1, @@ -69,9 +66,10 @@ public async Task MockServerTest() }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetStepPaginationTest.cs b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetStepPaginationTest.cs index 374471bcc9a..62c4c97b604 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetStepPaginationTest.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination.Test/Unit/MockServer/ListWithOffsetStepPaginationTest.cs @@ -1,9 +1,6 @@ using System.Threading.Tasks; -using FluentAssertions.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using SeedPagination; -using SeedPagination.Core; #nullable enable @@ -58,7 +55,7 @@ public async Task MockServerTest() .WithBody(mockResponse) ); - var response = await Client.Users.ListWithOffsetStepPaginationAsync( + var pager = Client.Users.ListWithOffsetStepPaginationAsync( new ListUsersOffsetStepPaginationRequest { Page = 1, @@ -67,9 +64,10 @@ public async Task MockServerTest() }, RequestOptions ); - JToken - .Parse(mockResponse) - .Should() - .BeEquivalentTo(JToken.Parse(JsonUtils.Serialize(response))); + await foreach (var item in pager) + { + Assert.That(item, Is.Not.Null); + break; // Only check the first item + } } } diff --git a/seed/csharp-sdk/pagination/src/SeedPagination/Core/Page.cs b/seed/csharp-sdk/pagination/src/SeedPagination/Core/Page.cs new file mode 100644 index 00000000000..856829ee3e7 --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination/Core/Page.cs @@ -0,0 +1,19 @@ +namespace SeedPagination.Core; + +/// +/// A single of items from a request that may return +/// zero or more s of items. +/// +/// The type of items. +public class Page +{ + public Page(IReadOnlyList items) + { + Items = items; + } + + /// + /// Gets the items in this . + /// + public IReadOnlyList Items { get; } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination/Core/Pager.cs b/seed/csharp-sdk/pagination/src/SeedPagination/Core/Pager.cs new file mode 100644 index 00000000000..aa020c21211 --- /dev/null +++ b/seed/csharp-sdk/pagination/src/SeedPagination/Core/Pager.cs @@ -0,0 +1,207 @@ +using System.Runtime.CompilerServices; + +namespace SeedPagination.Core; + +/// +/// A collection of values that may take multiple service requests to +/// iterate over. +/// +/// The type of the values. +public abstract class Pager : IAsyncEnumerable +{ + /// + /// Enumerate the values a at a time. This may + /// make multiple service requests. + /// + /// + /// An async sequence of s. + /// + public abstract IAsyncEnumerable> AsPagesAsync( + CancellationToken cancellationToken = default + ); + + /// + /// Enumerate the values in the collection asynchronously. This may + /// make multiple service requests. + /// + /// + /// The used for requests made while + /// enumerating asynchronously. + /// + /// An async sequence of values. + public virtual async IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) + { + await foreach (var page in AsPagesAsync(cancellationToken).ConfigureAwait(false)) + { + foreach (var value in page.Items) + { + yield return value; + } + } + } +} + +internal sealed class OffsetPager + : Pager +{ + private TRequest _request; + private readonly TRequestOptions? _options; + private readonly GetNextPage _getNextPage; + private readonly GetOffset _getOffset; + private readonly SetOffset _setOffset; + private readonly GetStep? _getStep; + private readonly GetItems _getItems; + private readonly HasNextPage? _hasNextPage; + + internal delegate Task GetNextPage( + TRequest request, + TRequestOptions? options, + CancellationToken cancellationToken + ); + + internal delegate TOffset GetOffset(TRequest request); + + internal delegate void SetOffset(TRequest request, TOffset offset); + + internal delegate TStep GetStep(TRequest request); + + internal delegate IReadOnlyList? GetItems(TResponse response); + + internal delegate bool? HasNextPage(TResponse response); + + internal OffsetPager( + TRequest request, + TRequestOptions? options, + GetNextPage getNextPage, + GetOffset getOffset, + SetOffset setOffset, + GetStep? getStep, + GetItems getItems, + HasNextPage? hasNextPage + ) + { + _request = request; + _options = options; + _getNextPage = getNextPage; + _getOffset = getOffset; + _setOffset = setOffset; + _getStep = getStep; + _getItems = getItems; + _hasNextPage = hasNextPage; + } + + public override async IAsyncEnumerable> AsPagesAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + var hasStep = false; + if (_getStep is not null) + { + hasStep = _getStep(_request) is not null; + } + var offset = _getOffset(_request); + var longOffset = Convert.ToInt64(offset); + bool hasNextPage; + do + { + var response = await _getNextPage(_request, _options, cancellationToken) + .ConfigureAwait(false); + var items = _getItems(response); + var itemCount = items?.Count ?? 0; + hasNextPage = _hasNextPage?.Invoke(response) ?? itemCount > 0; + if (items is not null) + { + yield return new Page(items); + } + + if (hasStep) + { + longOffset += items?.Count ?? 1; + } + else + { + longOffset++; + } + + _request ??= Activator.CreateInstance(); + switch (offset) + { + case int: + _setOffset(_request, (TOffset)(object)(int)longOffset); + break; + case long: + _setOffset(_request, (TOffset)(object)longOffset); + break; + default: + throw new InvalidOperationException("Offset must be int or long"); + } + } while (hasNextPage); + } +} + +internal sealed class CursorPager + : Pager +{ + private TRequest _request; + private readonly TRequestOptions? _options; + private readonly GetNextPage _getNextPage; + private readonly SetCursor _setCursor; + private readonly GetNextCursor _getNextCursor; + private readonly GetItems _getItems; + + internal delegate Task GetNextPage( + TRequest request, + TRequestOptions? options, + CancellationToken cancellationToken + ); + + internal delegate void SetCursor(TRequest request, TCursor cursor); + + internal delegate TCursor? GetNextCursor(TResponse response); + + internal delegate IReadOnlyList? GetItems(TResponse response); + + internal CursorPager( + TRequest request, + TRequestOptions? options, + GetNextPage getNextPage, + SetCursor setCursor, + GetNextCursor getNextCursor, + GetItems getItems + ) + { + _request = request; + _options = options; + _getNextPage = getNextPage; + _setCursor = setCursor; + _getNextCursor = getNextCursor; + _getItems = getItems; + } + + public override async IAsyncEnumerable> AsPagesAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + do + { + var response = await _getNextPage(_request, _options, cancellationToken) + .ConfigureAwait(false); + var items = _getItems(response); + var nextCursor = _getNextCursor(response); + if (items != null) + { + yield return new Page(items); + } + + if (nextCursor == null) + { + break; + } + + _request ??= Activator.CreateInstance(); + _setCursor(_request, nextCursor); + } while (true); + } +} diff --git a/seed/csharp-sdk/pagination/src/SeedPagination/Users/UsersClient.cs b/seed/csharp-sdk/pagination/src/SeedPagination/Users/UsersClient.cs index 314aefdc2ff..bd8ccbbc136 100644 --- a/seed/csharp-sdk/pagination/src/SeedPagination/Users/UsersClient.cs +++ b/seed/csharp-sdk/pagination/src/SeedPagination/Users/UsersClient.cs @@ -29,7 +29,391 @@ internal UsersClient(RawClient client) /// ); /// /// - public async Task ListWithCursorPaginationAsync( + public Pager ListWithCursorPaginationAsync( + ListUsersCursorPaginationRequest request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new CursorPager< + ListUsersCursorPaginationRequest, + RequestOptions?, + ListUsersPaginationResponse, + string, + User + >( + request, + options, + ListWithCursorPaginationAsync, + (request, cursor) => + { + request.StartingAfter = cursor; + }, + response => response?.Page?.Next?.StartingAfter, + response => response?.Data?.ToList() + ); + return pager; + } + + /// + /// + /// await client.Users.ListWithBodyCursorPaginationAsync( + /// new ListUsersBodyCursorPaginationRequest { Pagination = new WithCursor { Cursor = "cursor" } } + /// ); + /// + /// + public Pager ListWithBodyCursorPaginationAsync( + ListUsersBodyCursorPaginationRequest request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new CursorPager< + ListUsersBodyCursorPaginationRequest, + RequestOptions?, + ListUsersPaginationResponse, + string, + User + >( + request, + options, + ListWithBodyCursorPaginationAsync, + (request, cursor) => + { + request.Pagination ??= new(); + request.Pagination.Cursor = cursor; + }, + response => response?.Page?.Next?.StartingAfter, + response => response?.Data?.ToList() + ); + return pager; + } + + /// + /// + /// await client.Users.ListWithOffsetPaginationAsync( + /// new ListUsersOffsetPaginationRequest + /// { + /// Page = 1, + /// PerPage = 1, + /// Order = Order.Asc, + /// StartingAfter = "starting_after", + /// } + /// ); + /// + /// + public Pager ListWithOffsetPaginationAsync( + ListUsersOffsetPaginationRequest request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new OffsetPager< + ListUsersOffsetPaginationRequest, + RequestOptions?, + ListUsersPaginationResponse, + int?, + object, + User + >( + request, + options, + ListWithOffsetPaginationAsync, + request => request?.Page ?? 0, + (request, offset) => + { + request.Page = offset; + }, + null, + response => response?.Data?.ToList(), + null + ); + return pager; + } + + /// + /// + /// await client.Users.ListWithBodyOffsetPaginationAsync( + /// new ListUsersBodyOffsetPaginationRequest { Pagination = new WithPage { Page = 1 } } + /// ); + /// + /// + public Pager ListWithBodyOffsetPaginationAsync( + ListUsersBodyOffsetPaginationRequest request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new OffsetPager< + ListUsersBodyOffsetPaginationRequest, + RequestOptions?, + ListUsersPaginationResponse, + int?, + object, + User + >( + request, + options, + ListWithBodyOffsetPaginationAsync, + request => request?.Pagination?.Page ?? 0, + (request, offset) => + { + request.Pagination ??= new(); + request.Pagination.Page = offset; + }, + null, + response => response?.Data?.ToList(), + null + ); + return pager; + } + + /// + /// + /// await client.Users.ListWithOffsetStepPaginationAsync( + /// new ListUsersOffsetStepPaginationRequest + /// { + /// Page = 1, + /// Limit = 1, + /// Order = Order.Asc, + /// } + /// ); + /// + /// + public Pager ListWithOffsetStepPaginationAsync( + ListUsersOffsetStepPaginationRequest request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new OffsetPager< + ListUsersOffsetStepPaginationRequest, + RequestOptions?, + ListUsersPaginationResponse, + int?, + int?, + User + >( + request, + options, + ListWithOffsetStepPaginationAsync, + request => request?.Page ?? 0, + (request, offset) => + { + request.Page = offset; + }, + request => request?.Limit ?? 0, + response => response?.Data?.ToList(), + null + ); + return pager; + } + + /// + /// + /// await client.Users.ListWithOffsetPaginationHasNextPageAsync( + /// new ListWithOffsetPaginationHasNextPageRequest + /// { + /// Page = 1, + /// Limit = 1, + /// Order = Order.Asc, + /// } + /// ); + /// + /// + public Pager ListWithOffsetPaginationHasNextPageAsync( + ListWithOffsetPaginationHasNextPageRequest request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new OffsetPager< + ListWithOffsetPaginationHasNextPageRequest, + RequestOptions?, + ListUsersPaginationResponse, + int?, + int?, + User + >( + request, + options, + ListWithOffsetPaginationHasNextPageAsync, + request => request?.Page ?? 0, + (request, offset) => + { + request.Page = offset; + }, + request => request?.Limit ?? 0, + response => response?.Data?.ToList(), + response => response?.HasNextPage + ); + return pager; + } + + /// + /// + /// await client.Users.ListWithExtendedResultsAsync( + /// new ListUsersExtendedRequest { Cursor = "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" } + /// ); + /// + /// + public Pager ListWithExtendedResultsAsync( + ListUsersExtendedRequest request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new CursorPager< + ListUsersExtendedRequest, + RequestOptions?, + ListUsersExtendedResponse, + string?, + User + >( + request, + options, + ListWithExtendedResultsAsync, + (request, cursor) => + { + request.Cursor = cursor; + }, + response => response?.Next, + response => response?.Data?.Users?.ToList() + ); + return pager; + } + + /// + /// + /// await client.Users.ListWithExtendedResultsAndOptionalDataAsync( + /// new ListUsersExtendedRequestForOptionalData { Cursor = "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" } + /// ); + /// + /// + public Pager ListWithExtendedResultsAndOptionalDataAsync( + ListUsersExtendedRequestForOptionalData request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new CursorPager< + ListUsersExtendedRequestForOptionalData, + RequestOptions?, + ListUsersExtendedOptionalListResponse, + string?, + User + >( + request, + options, + ListWithExtendedResultsAndOptionalDataAsync, + (request, cursor) => + { + request.Cursor = cursor; + }, + response => response?.Next, + response => response?.Data?.Users?.ToList() + ); + return pager; + } + + /// + /// + /// await client.Users.ListUsernamesAsync( + /// new ListUsernamesRequest { StartingAfter = "starting_after" } + /// ); + /// + /// + public Pager ListUsernamesAsync( + ListUsernamesRequest request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new CursorPager< + ListUsernamesRequest, + RequestOptions?, + UsernameCursor, + string?, + string + >( + request, + options, + ListUsernamesAsync, + (request, cursor) => + { + request.StartingAfter = cursor; + }, + response => response?.Cursor?.After, + response => response?.Cursor?.Data?.ToList() + ); + return pager; + } + + /// + /// + /// await client.Users.ListWithGlobalConfigAsync(new ListWithGlobalConfigRequest { Offset = 1 }); + /// + /// + public Pager ListWithGlobalConfigAsync( + ListWithGlobalConfigRequest request, + RequestOptions? options = null + ) + { + if (request is not null) + { + request = request with { }; + } + var pager = new OffsetPager< + ListWithGlobalConfigRequest, + RequestOptions?, + UsernameContainer, + int?, + object, + string + >( + request, + options, + ListWithGlobalConfigAsync, + request => request?.Offset ?? 0, + (request, offset) => + { + request.Offset = offset; + }, + null, + response => response?.Results?.ToList(), + null + ); + return pager; + } + + internal async Task ListWithCursorPaginationAsync( ListUsersCursorPaginationRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -83,14 +467,7 @@ public async Task ListWithCursorPaginationAsync( ); } - /// - /// - /// await client.Users.ListWithBodyCursorPaginationAsync( - /// new ListUsersBodyCursorPaginationRequest { Pagination = new WithCursor { Cursor = "cursor" } } - /// ); - /// - /// - public async Task ListWithBodyCursorPaginationAsync( + internal async Task ListWithBodyCursorPaginationAsync( ListUsersBodyCursorPaginationRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -127,20 +504,7 @@ public async Task ListWithBodyCursorPaginationAsync ); } - /// - /// - /// await client.Users.ListWithOffsetPaginationAsync( - /// new ListUsersOffsetPaginationRequest - /// { - /// Page = 1, - /// PerPage = 1, - /// Order = Order.Asc, - /// StartingAfter = "starting_after", - /// } - /// ); - /// - /// - public async Task ListWithOffsetPaginationAsync( + internal async Task ListWithOffsetPaginationAsync( ListUsersOffsetPaginationRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -194,14 +558,7 @@ public async Task ListWithOffsetPaginationAsync( ); } - /// - /// - /// await client.Users.ListWithBodyOffsetPaginationAsync( - /// new ListUsersBodyOffsetPaginationRequest { Pagination = new WithPage { Page = 1 } } - /// ); - /// - /// - public async Task ListWithBodyOffsetPaginationAsync( + internal async Task ListWithBodyOffsetPaginationAsync( ListUsersBodyOffsetPaginationRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -238,19 +595,7 @@ public async Task ListWithBodyOffsetPaginationAsync ); } - /// - /// - /// await client.Users.ListWithOffsetStepPaginationAsync( - /// new ListUsersOffsetStepPaginationRequest - /// { - /// Page = 1, - /// Limit = 1, - /// Order = Order.Asc, - /// } - /// ); - /// - /// - public async Task ListWithOffsetStepPaginationAsync( + internal async Task ListWithOffsetStepPaginationAsync( ListUsersOffsetStepPaginationRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -300,19 +645,7 @@ public async Task ListWithOffsetStepPaginationAsync ); } - /// - /// - /// await client.Users.ListWithOffsetPaginationHasNextPageAsync( - /// new ListWithOffsetPaginationHasNextPageRequest - /// { - /// Page = 1, - /// Limit = 1, - /// Order = Order.Asc, - /// } - /// ); - /// - /// - public async Task ListWithOffsetPaginationHasNextPageAsync( + internal async Task ListWithOffsetPaginationHasNextPageAsync( ListWithOffsetPaginationHasNextPageRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -362,14 +695,7 @@ public async Task ListWithOffsetPaginationHasNextPa ); } - /// - /// - /// await client.Users.ListWithExtendedResultsAsync( - /// new ListUsersExtendedRequest { Cursor = "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" } - /// ); - /// - /// - public async Task ListWithExtendedResultsAsync( + internal async Task ListWithExtendedResultsAsync( ListUsersExtendedRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -411,14 +737,7 @@ public async Task ListWithExtendedResultsAsync( ); } - /// - /// - /// await client.Users.ListWithExtendedResultsAndOptionalDataAsync( - /// new ListUsersExtendedRequestForOptionalData { Cursor = "d5e9c84f-c2b2-4bf4-b4b0-7ffd7a9ffc32" } - /// ); - /// - /// - public async Task ListWithExtendedResultsAndOptionalDataAsync( + internal async Task ListWithExtendedResultsAndOptionalDataAsync( ListUsersExtendedRequestForOptionalData request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -460,14 +779,7 @@ public async Task ListWithExtendedResults ); } - /// - /// - /// await client.Users.ListUsernamesAsync( - /// new ListUsernamesRequest { StartingAfter = "starting_after" } - /// ); - /// - /// - public async Task ListUsernamesAsync( + internal async Task ListUsernamesAsync( ListUsernamesRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -509,12 +821,7 @@ public async Task ListUsernamesAsync( ); } - /// - /// - /// await client.Users.ListWithGlobalConfigAsync(new ListWithGlobalConfigRequest { Offset = 1 }); - /// - /// - public async Task ListWithGlobalConfigAsync( + internal async Task ListWithGlobalConfigAsync( ListWithGlobalConfigRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default diff --git a/test-definitions/fern/apis/pagination/definition/users.yml b/test-definitions/fern/apis/pagination/definition/users.yml index ecde10ef707..ddc0bc1848b 100644 --- a/test-definitions/fern/apis/pagination/definition/users.yml +++ b/test-definitions/fern/apis/pagination/definition/users.yml @@ -1,9 +1,9 @@ imports: root: __package__.yml -types: - Order: - enum: +types: + Order: + enum: - asc - desc @@ -15,8 +15,8 @@ types: properties: cursor: optional - UserListContainer: - properties: + UserListContainer: + properties: users: list UserPage: @@ -24,60 +24,59 @@ types: data: UserListContainer next: optional - UserOptionalListContainer: - properties: + UserOptionalListContainer: + properties: users: optional> UserOptionalListPage: properties: data: UserOptionalListContainer next: optional - UsernameContainer: properties: results: list - ListUsersExtendedResponse: + ListUsersExtendedResponse: extends: - UserPage properties: - total_count: + total_count: type: integer docs: The totall number of /users - ListUsersExtendedOptionalListResponse: + ListUsersExtendedOptionalListResponse: extends: - UserOptionalListPage properties: - total_count: + total_count: type: integer docs: The totall number of /users - - ListUsersPaginationResponse: - properties: + + ListUsersPaginationResponse: + properties: hasNextPage: optional page: optional - total_count: + total_count: type: integer docs: The totall number of /users data: list - Page: - properties: - page: + Page: + properties: + page: type: integer docs: The current page next: optional per_page: integer total_page: integer - NextPage: - properties: + NextPage: + properties: page: integer starting_after: string - User: - properties: + User: + properties: name: string id: integer @@ -86,7 +85,7 @@ service: base-path: /users endpoints: listWithCursorPagination: - pagination: + pagination: cursor: $request.starting_after next_cursor: $response.page.next.starting_after results: $response.data @@ -95,41 +94,41 @@ service: request: name: ListUsersCursorPaginationRequest query-parameters: - page: + page: type: optional docs: Defaults to first page - per_page: + per_page: type: optional docs: Defaults to per page - order: - type: optional - starting_after: + order: + type: optional + starting_after: type: optional - docs: | - The cursor used for pagination in order to fetch + docs: | + The cursor used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse listWithBodyCursorPagination: - pagination: + pagination: cursor: $request.pagination.cursor next_cursor: $response.page.next.starting_after results: $response.data method: POST path: "" - request: + request: name: ListUsersBodyCursorPaginationRequest - body: + body: properties: - pagination: + pagination: type: optional - docs: | + docs: | The object that contains the cursor used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse listWithOffsetPagination: - pagination: + pagination: offset: $request.page results: $response.data method: GET @@ -137,38 +136,37 @@ service: request: name: ListUsersOffsetPaginationRequest query-parameters: - page: + page: type: optional docs: Defaults to first page - per_page: + per_page: type: optional docs: Defaults to per page - order: - type: optional - starting_after: + order: + type: optional + starting_after: type: optional - docs: | - The cursor used for pagination in order to fetch + docs: | + The cursor used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse listWithBodyOffsetPagination: - pagination: + pagination: offset: $request.pagination.page results: $response.data method: POST path: "" - request: + request: name: ListUsersBodyOffsetPaginationRequest - body: + body: properties: - pagination: + pagination: type: optional - docs: | + docs: | The object that contains the offset used for pagination in order to fetch the next page of results. response: ListUsersPaginationResponse - listWithOffsetStepPagination: pagination: offset: $request.page @@ -191,8 +189,8 @@ service: order: type: optional response: ListUsersPaginationResponse - - listWithOffsetPaginationHasNextPage: + + listWithOffsetPaginationHasNextPage: pagination: offset: $request.page results: $response.data @@ -217,7 +215,7 @@ service: response: ListUsersPaginationResponse listWithExtendedResults: - pagination: + pagination: cursor: $request.cursor next_cursor: $response.next results: $response.data.users @@ -230,7 +228,7 @@ service: response: ListUsersExtendedResponse listWithExtendedResultsAndOptionalData: - pagination: + pagination: cursor: $request.cursor next_cursor: $response.next results: $response.data.users @@ -243,7 +241,7 @@ service: response: ListUsersExtendedOptionalListResponse listUsernames: - pagination: + pagination: cursor: $request.starting_after next_cursor: $response.cursor.after results: $response.cursor.data @@ -252,10 +250,10 @@ service: request: name: ListUsernamesRequest query-parameters: - starting_after: + starting_after: type: optional - docs: | - The cursor used for pagination in order to fetch + docs: | + The cursor used for pagination in order to fetch the next page of results. response: root.UsernameCursor @@ -266,6 +264,6 @@ service: request: name: ListWithGlobalConfigRequest query-parameters: - offset: + offset: type: optional response: UsernameContainer \ No newline at end of file