Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(csharp): add pagination helper methods to C# SDK generator #5187

Merged
merged 22 commits into from
Nov 19, 2024

Conversation

Swimburger
Copy link
Member

This feature works, but the code is still a little rough and needs some cleaning.

  • The new HttpPagerEndpointGenerator generates pager methods that use the main endpoint methods for users to paginate over items in a paginated endpoint.
  • AsIs/Page.Template.cs holds a list of items in a single page
  • AsIs/Pager.Template.cs has a public abstract Pager<Item> class for a user to interact with, and two internal implementations OffsetPager and CursorPager`. The bulk of the logic to paginate is stored within these implementations, and the remaining parts are passed into the pagers as delegates.

⚠️ Assumption: For cursor-based pagination, the cursor is assumed to be a string. This is probably correct in 95% of cases, but that's just a guess. Does the cursor type need to be configurable?

⚠️ Big change: The generator thus far assumes that each endpoint generates one method on a client. This PR changes that so that an endpoint generator can generate multiple methods. For generated SDKs, this isn't too big of a change, but this also affects generated snippets and references. There's some code duplication to quickly get snippets and references to work as expected with this pager variation method for the endpoint

  1. Should we stick to one method per endpoint to reduce the amount of change in this generator and downstream dependencies? Or
  2. Should we push forward on an endpoint with multiple methods and refactor the generator at a larger scale for this to be supported seamlessly?

* Refactor to take into account different RequestOptions
* Generate snippets
* Generate reference
Copy link

github-actions bot commented Nov 15, 2024

@Swimburger
Copy link
Member Author

Decisions:

  • Make original method private/internal
  • Only make pager public and use the original method name
  • Produce snippets/references only with pager name

@Swimburger
Copy link
Member Author

The mockResponse is generated incorrectly in ListWithExtendedResultsAndOptionalDataTest and ListWithExtendedResultsTest.
The mockresponse JSON should have the data property. The list within data is optional so that's fine, but there should be a data: {} property since that isn't optional in Fern definition.

@Swimburger Swimburger marked this pull request as ready for review November 18, 2024 19:32
@Swimburger Swimburger changed the title feat(csharp): WIP add pagination helper methods to C# SDK generator feat(csharp): add pagination helper methods to C# SDK generator Nov 18, 2024
* Support different types in cursor pagination
* Support int and long in offset pagination
* Initialize request if null and nested properties towards setCursor and setOffset
@Swimburger
Copy link
Member Author

Improvements:

  • Support for different types in cursor pagination
  • Support for int and long in offset pagination
  • Initialize request if null before passing request to setCursor and setOffset
  • Initialize nested properties towards cursor and offset property.

For initializing towards cursor and offset property, I assume the types don't have required fields and initialize an empty instance using ?? = new().

I'm worried if one of these types does have a required field, at which point the compiler will complain.

Copy link
Contributor

@amckinney amckinney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looking great - mostly comments on implementation, and only minor details for the generated code (e.g. comments, etc).

offset: (pagination) => pagination.results.property.valueType,
cursor: (pagination) => pagination.results.property.valueType,
_other: (pagination) => {
throw new Error(`Unsupported pagination type: ${pagination.type}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer switch w/ assertNever if possible.

Comment on lines 439 to 441
_other: (pagination) => {
throw new Error(`Unsupported pagination type: ${pagination.type}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Prefer switch w/ assertNever here.

writer.writeTextStatement(")");
writer.writeTextStatement("return pager");
},
cursor: (pagination) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Could we put all of this in a separate function (e.g. generateCursorPagerMethod).

}

endpoint.pagination?._visit({
offset: (pagination) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Could we put all of this in a separate function (e.g. generateOffsetPagerMethod).

Comment on lines +69 to +88
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated code looks great 👏

Comment on lines +483 to +520
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
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👏

Comment on lines +325 to +333
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 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of parameters required here - I wonder if it's be worth rolling out a PaginatedEndpointGenerator class (or something similar) that could define all these properties as member variables, so that the following implementation could be reduced / a little more concise and readable.

Not totally necessary to do that here, but let me know your thoughts.

@Swimburger
Copy link
Member Author

I added a shallow clone using the record with {} feature. Future versions of C# may include a DeepClone feature for records.

Copy link
Contributor

@amckinney amckinney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!! 🎉

.vscode/settings.json Outdated Show resolved Hide resolved
Comment on lines +489 to +510
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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should we just return the pager directly rather than using var?

Comment on lines +427 to +456
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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Same thing with returning directly.

Swimburger and others added 2 commits November 19, 2024 11:01
Co-authored-by: Alex McKinney <alexmckinney01@gmail.com>
@Swimburger Swimburger enabled auto-merge (squash) November 19, 2024 20:59
@Swimburger Swimburger merged commit 339cc01 into main Nov 19, 2024
57 of 59 checks passed
@Swimburger Swimburger deleted the niels/csharp/pagination branch November 19, 2024 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants