Skip to content

Commit cb329da

Browse files
authored
Update codegen for breaking changes in pageable methods (#758)
* Update codegen for breaking changes in pageable methods Updated code model to have discrete types for ClientMethodOptions, PagerOptions, and PollerOptions from azure_core. The constructor for Pager now requires a PagerContinuationKind argument that's used to determine the emitted generic type parameters. * update to official commit * set release date
1 parent fddebae commit cb329da

29 files changed

Lines changed: 260 additions & 160 deletions

File tree

packages/typespec-rust/CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Release History
22

3-
## 0.31.1 (unreleased)
3+
## 0.32.0 (2025-12-11)
4+
5+
### Breaking Changes
6+
7+
**Note this version is incompatible with earlier versions of `azure_core`**
8+
9+
* Replaced `Pager::from_callback` with `Pager::new`.
10+
* Updated generic type parameters to `Pager<>` and `PagerOptions<>` type as required by the paging strategy.
411

512
### Features Added
613

packages/typespec-rust/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@azure-tools/typespec-rust",
3-
"version": "0.31.1",
3+
"version": "0.32.0",
44
"description": "TypeSpec emitter for Rust SDKs",
55
"type": "module",
66
"packageManager": "pnpm@10.10.0",

packages/typespec-rust/src/codegen/clients.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ function getMethodOptions(crate: rust.Crate): helpers.Module {
349349
body += `${indent.push().get()}${method.options.type.name} {\n`;
350350
indent.push();
351351
for (const field of method.options.type.fields) {
352-
if (field.type.kind === 'external' && (field.type.name === 'ClientMethodOptions' || field.type.name === 'PagerOptions' || field.type.name === 'PollerOptions')) {
352+
if (field.type.kind === 'clientMethodOptions' || field.type.kind === 'pagerOptions' || field.type.kind === 'pollerOptions') {
353353
body += `${indent.get()}${field.name}: ${field.type.name} {\n`;
354354
body += `${indent.push().get()}context: self.${field.name}.context.into_owned(),\n`;
355355
body += `${indent.get()}..self.${field.name}\n`;
@@ -1267,7 +1267,7 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru
12671267
switch (method.strategy.kind) {
12681268
case 'continuationToken': {
12691269
const reqTokenParam = method.strategy.requestToken.name;
1270-
body += `${indent.get()}Ok(${method.returns.type.name}::from_callback(move |${reqTokenParam}: PagerState<String>, pager_options| {\n`;
1270+
body += `${indent.get()}Ok(${method.returns.type.name}::new(move |${reqTokenParam}: PagerState<String>, pager_options| {\n`;
12711271
body += `${indent.push().get()}let ${method.strategy.requestToken.kind === 'queryScalar' ? 'mut ' : ''}url = first_url.clone();\n`;
12721272
if (method.strategy.requestToken.kind === 'queryScalar') {
12731273
// if the url already contains the token query param,
@@ -1290,7 +1290,7 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru
12901290
case 'nextLink': {
12911291
const nextLinkName = method.strategy.nextLinkPath[method.strategy.nextLinkPath.length - 1].name;
12921292
const reinjectedParams = method.strategy.reinjectedParams;
1293-
body += `${indent.get()}Ok(${method.returns.type.name}::from_callback(move |${nextLinkName}: PagerState<Url>, pager_options| {\n`;
1293+
body += `${indent.get()}Ok(${method.returns.type.name}::new(move |${nextLinkName}: PagerState<Url>, pager_options| {\n`;
12941294
body += `${indent.push().get()}let url = ` + helpers.buildMatch(indent, nextLinkName, [{
12951295
pattern: `PagerState::More(${nextLinkName})`,
12961296
body: (indent) => {
@@ -1336,7 +1336,7 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru
13361336
}
13371337
} else {
13381338
// no next link when there's no strategy
1339-
body += `${indent.get()}Ok(Pager::from_callback(move |_: PagerState<Url>, pager_options| {\n`;
1339+
body += `${indent.get()}Ok(${method.returns.type.name}::new(move |_: PagerState<Url>, pager_options| {\n`;
13401340
indent.push();
13411341
cloneUrl = true;
13421342
srcUrlVar = urlVar;
@@ -1357,7 +1357,7 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru
13571357
const requestResult = constructRequest(indent, use, method, paramGroups, true, srcUrlVar, cloneUrl);
13581358
body += requestResult.content;
13591359
body += `${indent.get()}let pipeline = pipeline.clone();\n`;
1360-
body += `${indent.get()}async move {\n`;
1360+
body += `${indent.get()}Box::pin(async move {\n`;
13611361
body += `${indent.push().get()}let rsp${rspType} = pipeline.send(&pager_options.context, &mut ${requestResult.requestVarName}, ${getPipelineOptions(indent, use, method)}).await?${rspInto};\n`;
13621362

13631363
// check if we need to extract the next link field from the response model
@@ -1436,9 +1436,9 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru
14361436
body += `${indent.get()}Ok(PagerResult::Done { response: rsp.into() })\n`;
14371437
}
14381438

1439-
body += `${indent.pop().get()}}\n`; // end async move
1440-
body += `${indent.get()}},\n${indent.get()}Some(options.method_options),\n`; // end move
1441-
body += `${indent.pop().get()}))`; // end Ok/Pager::from_callback
1439+
body += `${indent.pop().get()}})\n`; // end Box::pin(async move {
1440+
body += `${indent.get()}},\n${indent.get()}Some(options.method_options),\n`; // end move {
1441+
body += `${indent.pop().get()}))`; // end Ok(Pager::new(
14421442

14431443
return body;
14441444
}

packages/typespec-rust/src/codegen/helpers.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ export function getTypeDeclaration(type: rust.Client | rust.Payload | rust.Respo
164164
case 'decimal':
165165
case 'marker':
166166
return type.name;
167+
case 'clientMethodOptions':
168+
case 'pollerOptions':
169+
return `${type.name}${getGenericLifetimeAnnotation(type.lifetime)}`;
167170
case 'encodedBytes':
168171
return type.slice ? '[u8]' : 'Vec<u8>';
169172
case 'enumValue':
@@ -178,9 +181,22 @@ export function getTypeDeclaration(type: rust.Client | rust.Payload | rust.Respo
178181
return getTypeDeclaration(type.valueKind);
179182
case 'option':
180183
return `Option<${getTypeDeclaration(type.type, withLifetime)}>`;
181-
case 'pager':
184+
case 'pager': {
185+
let formatParam = '';
186+
let continuationParam = '';
187+
// we need a third generic type param when the continuation isn't a next link
188+
if (type.continuation !== 'nextLink') {
189+
formatParam = `, ${type.type.format}`;
190+
continuationParam = ', String';
191+
} else if (type.type.format !== 'JsonFormat') {
192+
formatParam = `, ${type.type.format}`;
193+
}
182194
// we explicitly omit the Response<T> from the type decl
183-
return `Pager<${getTypeDeclaration(type.type.content, withLifetime)}${type.type.format !== 'JsonFormat' ? `, ${type.type.format}` : ''}>`;
195+
return `Pager<${getTypeDeclaration(type.type.content, withLifetime)}${formatParam}${continuationParam}>`;
196+
}
197+
case 'pagerOptions':
198+
// for continuation tokens we need an extra generic type parameter
199+
return `${type.name}<${type.lifetime.name}${type.continuation === 'nextLink' ? '' : ', String'}>`;
184200
case 'poller':
185201
// we explicitly omit the Response<T> from the type decl
186202
return `Poller<${getTypeDeclaration(type.type.content, withLifetime)}>`;

packages/typespec-rust/src/codegen/use.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ export class Use {
116116
case 'Vec':
117117
this.addForType(type.type);
118118
break;
119+
case 'pager':
120+
if (type.continuation !== 'nextLink') {
121+
// continuation token strategy will require the C
122+
// type param in Pager<'a, F, C> so we must bring
123+
// the format type into scope
124+
this.add('azure_core::http', type.type.format);
125+
}
126+
break;
119127
case 'payload':
120128
this.addForType(type.type);
121129
break;

packages/typespec-rust/src/codemodel/types.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface Docs {
1515
}
1616

1717
/** SdkType defines types used in generated code but do not directly participate in serde */
18-
export type SdkType = Arc | AsyncResponse | Box | ImplTrait | MarkerType | Option | Pager | Poller | RawResponse | RequestContent | Response | Result | Struct | TokenCredential | Unit;
18+
export type SdkType = Arc | AsyncResponse | Box | ClientMethodOptions | ImplTrait | MarkerType | Option | Pager | PagerOptions | Poller | PollerOptions | RawResponse | RequestContent | Response | Result | Struct | TokenCredential | Unit;
1919

2020
/** WireType defines types that go across the wire */
2121
export type WireType = Bytes | Decimal | DiscriminatedUnion | EncodedBytes | Enum | EnumValue | Etag | ExternalType | HashMap | JsonValue | Literal | Model | OffsetDateTime | RefBase | SafeInt | Scalar | Slice | StringSlice | StringType | Url | Vector;
@@ -58,6 +58,14 @@ export interface Bytes extends External {
5858
kind: 'bytes';
5959
}
6060

61+
/** ClientMethodOptions is a ClientMethodOptions<'a> from azure_core */
62+
export interface ClientMethodOptions extends External {
63+
kind: 'clientMethodOptions';
64+
65+
/** the lifetime annotation */
66+
lifetime: Lifetime;
67+
}
68+
6169
/** Decimal is a rust_decimal::Decimal type */
6270
export interface Decimal extends External {
6371
kind: 'decimal';
@@ -330,6 +338,23 @@ export interface Pager extends External {
330338

331339
/** the model containing the page of items */
332340
type: Response<Model, Exclude<PayloadFormatType, 'NoFormat'>>;
341+
342+
/** the type of continuation used by the pager */
343+
continuation: PagerContinuationKind;
344+
}
345+
346+
/** PagerContinuationKind contains the kinds of paging continuations */
347+
export type PagerContinuationKind = 'token' | 'nextLink';
348+
349+
/** PagerOptions is a PagerOptions<'a, C> from azure_core */
350+
export interface PagerOptions extends External {
351+
kind: 'pagerOptions';
352+
353+
/** the lifetime annotation */
354+
lifetime: Lifetime;
355+
356+
/** the type of continuation used by the pager */
357+
continuation: PagerContinuationKind;
333358
}
334359

335360
/** Poller is a Poller<T> from azure_core */
@@ -343,6 +368,14 @@ export interface Poller extends External {
343368
type: Response<Model, Exclude<PayloadFormatType, 'NoFormat'>>;
344369
}
345370

371+
/** PollerOptions is a PollerOptions<'a> from azure_core */
372+
export interface PollerOptions extends External {
373+
kind: 'pollerOptions';
374+
375+
/** the lifetime annotation */
376+
lifetime: Lifetime;
377+
}
378+
346379
/** PayloadFormat indicates the wire format for request bodies */
347380
export type PayloadFormat = 'json' | 'xml';
348381

@@ -657,6 +690,14 @@ export class Bytes extends External implements Bytes {
657690
}
658691
}
659692

693+
export class ClientMethodOptions extends External implements ClientMethodOptions {
694+
constructor(crate: Crate, lifetime: Lifetime) {
695+
super(crate, 'ClientMethodOptions', 'azure_core::http');
696+
this.kind = 'clientMethodOptions';
697+
this.lifetime = lifetime;
698+
}
699+
}
700+
660701
export class Decimal extends External implements Decimal {
661702
constructor(crate: Crate, stringEncoding: boolean) {
662703
super(crate, 'Decimal', 'rust_decimal', !stringEncoding ? ['serde-with-float'] : undefined);
@@ -814,10 +855,20 @@ export class Option<T> implements Option<T> {
814855
}
815856

816857
export class Pager extends External implements Pager {
817-
constructor(crate: Crate, type: Response<Model, Exclude<PayloadFormatType, 'NoFormat'>>) {
858+
constructor(crate: Crate, type: Response<Model, Exclude<PayloadFormatType, 'NoFormat'>>, continuation: PagerContinuationKind) {
818859
super(crate, 'Pager', 'azure_core::http');
819860
this.kind = 'pager';
820861
this.type = type;
862+
this.continuation = continuation;
863+
}
864+
}
865+
866+
export class PagerOptions extends External implements PagerOptions {
867+
constructor(crate: Crate, lifetime: Lifetime, continuation: PagerContinuationKind) {
868+
super(crate, 'PagerOptions', 'azure_core::http::pager');
869+
this.kind = 'pagerOptions';
870+
this.lifetime = lifetime;
871+
this.continuation = continuation;
821872
}
822873
}
823874

@@ -829,6 +880,14 @@ export class Poller extends External implements Poller {
829880
}
830881
}
831882

883+
export class PollerOptions extends External implements PollerOptions {
884+
constructor(crate: Crate, lifetime: Lifetime) {
885+
super(crate, 'PollerOptions', 'azure_core::http::poller');
886+
this.kind = 'pollerOptions';
887+
this.lifetime = lifetime;
888+
}
889+
}
890+
832891
export class Payload<T> implements Payload<T> {
833892
constructor(type: T, format: PayloadFormat) {
834893
this.kind = 'payload';

packages/typespec-rust/src/tcgcadapter/adapter.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,19 +1309,19 @@ export class Adapter {
13091309
methodOptionsStruct.lifetime = optionsLifetime;
13101310
methodOptionsStruct.docs.summary = `Options to be passed to ${this.asDocLink(`${rustClient.name}::${methodName}()`, `crate::generated::clients::${rustClient.name}::${methodName}()`)}`;
13111311

1312-
let clientMethodOptions: rust.ExternalType;
1312+
let clientMethodOptions: rust.ClientMethodOptions | rust.PagerOptions | rust.PollerOptions;
13131313
switch (method.kind) {
13141314
case 'paging':
1315-
clientMethodOptions = new rust.ExternalType(this.crate, 'PagerOptions', 'azure_core::http::pager');
1315+
// default to nextLink. will update it as required when we have that info
1316+
clientMethodOptions = new rust.PagerOptions(this.crate, optionsLifetime, 'nextLink');
13161317
break;
13171318
case 'lro':
1318-
clientMethodOptions = new rust.ExternalType(this.crate, 'PollerOptions', 'azure_core::http::poller');
1319+
clientMethodOptions = new rust.PollerOptions(this.crate, optionsLifetime);
13191320
break;
13201321
default:
1321-
clientMethodOptions = new rust.ExternalType(this.crate, 'ClientMethodOptions', 'azure_core::http');
1322+
clientMethodOptions = new rust.ClientMethodOptions(this.crate, optionsLifetime);
13221323
}
13231324

1324-
clientMethodOptions.lifetime = optionsLifetime;
13251325
const methodOptionsField = new rust.StructField('method_options', 'pub', clientMethodOptions);
13261326
methodOptionsField.docs.summary = 'Allows customization of the method call.';
13271327
methodOptionsStruct.fields.push(methodOptionsField);
@@ -1606,7 +1606,8 @@ export class Adapter {
16061606
}
16071607

16081608
this.crate.addDependency(new rust.CrateDependency('async-trait'));
1609-
rustMethod.returns = new rust.Result(this.crate, new rust.Pager(this.crate, new rust.Response(this.crate, synthesizedModel, responseFormat)));
1609+
// default to nextLink. will update it as required when we have that info
1610+
rustMethod.returns = new rust.Result(this.crate, new rust.Pager(this.crate, new rust.Response(this.crate, synthesizedModel, responseFormat), 'nextLink'));
16101611
} else if (method.kind === 'lro') {
16111612
const format = responseFormat === 'NoFormat' ? 'JsonFormat' : responseFormat
16121613

@@ -1689,6 +1690,14 @@ export class Adapter {
16891690
pageableMethod.strategy = this.adaptPageableMethodStrategy(method, paramsMap, responseHeadersMap);
16901691
if (pageableMethod.strategy?.kind === 'nextLink') {
16911692
pageableMethod.strategy.reinjectedParams = this.adaptPageableMethodReinjectionParams(method, paramsMap);
1693+
} else if (pageableMethod.strategy?.kind === 'continuationToken') {
1694+
// set the continuation type to token on the Pager and the PagerOptions field in the method options
1695+
pageableMethod.returns.type.continuation = 'token';
1696+
for (const field of pageableMethod.options.type.fields) {
1697+
if (field.type.kind === 'pagerOptions') {
1698+
field.type.continuation = 'token';
1699+
}
1700+
}
16921701
}
16931702
}
16941703
}

packages/typespec-rust/test/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ rust-version = "1.85"
9797
[workspace.dependencies]
9898
# Third-party dependencies should be kept up to date with https://github.com/Azure/azure-sdk-for-rust/blob/main/Cargo.lock
9999
async-trait = "0.1.88"
100-
azure_core = { git = "https://github.com/Azure/azure-sdk-for-rust.git", rev = "1bc0ef3a9e27af4990435de23e1127155fbba68c", features = [
100+
azure_core = { git = "https://github.com/Azure/azure-sdk-for-rust.git", rev = "197ddcf43752a5d2b28ae77cb7433f2a6ebcb57e", features = [
101101
"decimal",
102102
"reqwest",
103103
] }

packages/typespec-rust/test/other/colliding_locals/src/generated/clients/colliding_locals_client.rs

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)