Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/typespec-rust/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Release History

## 0.31.1 (unreleased)
## 0.32.0 (2025-12-11)

### Breaking Changes

**Note this version is incompatible with earlier versions of `azure_core`**

* Replaced `Pager::from_callback` with `Pager::new`.
* Updated generic type parameters to `Pager<>` and `PagerOptions<>` type as required by the paging strategy.

### Features Added

Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-rust/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure-tools/typespec-rust",
"version": "0.31.1",
"version": "0.32.0",
"description": "TypeSpec emitter for Rust SDKs",
"type": "module",
"packageManager": "pnpm@10.10.0",
Expand Down
16 changes: 8 additions & 8 deletions packages/typespec-rust/src/codegen/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ function getMethodOptions(crate: rust.Crate): helpers.Module {
body += `${indent.push().get()}${method.options.type.name} {\n`;
indent.push();
for (const field of method.options.type.fields) {
if (field.type.kind === 'external' && (field.type.name === 'ClientMethodOptions' || field.type.name === 'PagerOptions' || field.type.name === 'PollerOptions')) {
if (field.type.kind === 'clientMethodOptions' || field.type.kind === 'pagerOptions' || field.type.kind === 'pollerOptions') {
body += `${indent.get()}${field.name}: ${field.type.name} {\n`;
body += `${indent.push().get()}context: self.${field.name}.context.into_owned(),\n`;
body += `${indent.get()}..self.${field.name}\n`;
Expand Down Expand Up @@ -1267,7 +1267,7 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru
switch (method.strategy.kind) {
case 'continuationToken': {
const reqTokenParam = method.strategy.requestToken.name;
body += `${indent.get()}Ok(${method.returns.type.name}::from_callback(move |${reqTokenParam}: PagerState<String>, pager_options| {\n`;
body += `${indent.get()}Ok(${method.returns.type.name}::new(move |${reqTokenParam}: PagerState<String>, pager_options| {\n`;
body += `${indent.push().get()}let ${method.strategy.requestToken.kind === 'queryScalar' ? 'mut ' : ''}url = first_url.clone();\n`;
if (method.strategy.requestToken.kind === 'queryScalar') {
// if the url already contains the token query param,
Expand All @@ -1290,7 +1290,7 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru
case 'nextLink': {
const nextLinkName = method.strategy.nextLinkPath[method.strategy.nextLinkPath.length - 1].name;
const reinjectedParams = method.strategy.reinjectedParams;
body += `${indent.get()}Ok(${method.returns.type.name}::from_callback(move |${nextLinkName}: PagerState<Url>, pager_options| {\n`;
body += `${indent.get()}Ok(${method.returns.type.name}::new(move |${nextLinkName}: PagerState<Url>, pager_options| {\n`;
body += `${indent.push().get()}let url = ` + helpers.buildMatch(indent, nextLinkName, [{
pattern: `PagerState::More(${nextLinkName})`,
body: (indent) => {
Expand Down Expand Up @@ -1336,7 +1336,7 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru
}
} else {
// no next link when there's no strategy
body += `${indent.get()}Ok(Pager::from_callback(move |_: PagerState<Url>, pager_options| {\n`;
body += `${indent.get()}Ok(${method.returns.type.name}::new(move |_: PagerState<Url>, pager_options| {\n`;
Comment thread
jhendrixMSFT marked this conversation as resolved.
indent.push();
cloneUrl = true;
srcUrlVar = urlVar;
Expand All @@ -1357,7 +1357,7 @@ function getPageableMethodBody(indent: helpers.indentation, use: Use, client: ru
const requestResult = constructRequest(indent, use, method, paramGroups, true, srcUrlVar, cloneUrl);
body += requestResult.content;
body += `${indent.get()}let pipeline = pipeline.clone();\n`;
body += `${indent.get()}async move {\n`;
body += `${indent.get()}Box::pin(async move {\n`;
body += `${indent.push().get()}let rsp${rspType} = pipeline.send(&pager_options.context, &mut ${requestResult.requestVarName}, ${getPipelineOptions(indent, use, method)}).await?${rspInto};\n`;

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

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

return body;
}
Expand Down
20 changes: 18 additions & 2 deletions packages/typespec-rust/src/codegen/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ export function getTypeDeclaration(type: rust.Client | rust.Payload | rust.Respo
case 'decimal':
case 'marker':
return type.name;
case 'clientMethodOptions':
case 'pollerOptions':
return `${type.name}${getGenericLifetimeAnnotation(type.lifetime)}`;
case 'encodedBytes':
return type.slice ? '[u8]' : 'Vec<u8>';
case 'enumValue':
Expand All @@ -178,9 +181,22 @@ export function getTypeDeclaration(type: rust.Client | rust.Payload | rust.Respo
return getTypeDeclaration(type.valueKind);
case 'option':
return `Option<${getTypeDeclaration(type.type, withLifetime)}>`;
case 'pager':
case 'pager': {
let formatParam = '';
let continuationParam = '';
// we need a third generic type param when the continuation isn't a next link
if (type.continuation !== 'nextLink') {
formatParam = `, ${type.type.format}`;
continuationParam = ', String';
} else if (type.type.format !== 'JsonFormat') {
formatParam = `, ${type.type.format}`;
}
// we explicitly omit the Response<T> from the type decl
return `Pager<${getTypeDeclaration(type.type.content, withLifetime)}${type.type.format !== 'JsonFormat' ? `, ${type.type.format}` : ''}>`;
return `Pager<${getTypeDeclaration(type.type.content, withLifetime)}${formatParam}${continuationParam}>`;
}
case 'pagerOptions':
// for continuation tokens we need an extra generic type parameter
return `${type.name}<${type.lifetime.name}${type.continuation === 'nextLink' ? '' : ', String'}>`;
case 'poller':
// we explicitly omit the Response<T> from the type decl
return `Poller<${getTypeDeclaration(type.type.content, withLifetime)}>`;
Expand Down
8 changes: 8 additions & 0 deletions packages/typespec-rust/src/codegen/use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ export class Use {
case 'Vec':
this.addForType(type.type);
break;
case 'pager':
if (type.continuation !== 'nextLink') {
// continuation token strategy will require the C
// type param in Pager<'a, F, C> so we must bring
// the format type into scope
this.add('azure_core::http', type.type.format);
}
break;
case 'payload':
this.addForType(type.type);
break;
Expand Down
63 changes: 61 additions & 2 deletions packages/typespec-rust/src/codemodel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface Docs {
}

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

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

/** ClientMethodOptions is a ClientMethodOptions<'a> from azure_core */
export interface ClientMethodOptions extends External {
kind: 'clientMethodOptions';

/** the lifetime annotation */
lifetime: Lifetime;
}

/** Decimal is a rust_decimal::Decimal type */
export interface Decimal extends External {
kind: 'decimal';
Expand Down Expand Up @@ -330,6 +338,23 @@ export interface Pager extends External {

/** the model containing the page of items */
type: Response<Model, Exclude<PayloadFormatType, 'NoFormat'>>;

/** the type of continuation used by the pager */
continuation: PagerContinuationKind;
}

/** PagerContinuationKind contains the kinds of paging continuations */
export type PagerContinuationKind = 'token' | 'nextLink';

/** PagerOptions is a PagerOptions<'a, C> from azure_core */
export interface PagerOptions extends External {
kind: 'pagerOptions';

/** the lifetime annotation */
lifetime: Lifetime;

/** the type of continuation used by the pager */
continuation: PagerContinuationKind;
}

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

/** PollerOptions is a PollerOptions<'a> from azure_core */
export interface PollerOptions extends External {
kind: 'pollerOptions';

/** the lifetime annotation */
lifetime: Lifetime;
}

/** PayloadFormat indicates the wire format for request bodies */
export type PayloadFormat = 'json' | 'xml';

Expand Down Expand Up @@ -657,6 +690,14 @@ export class Bytes extends External implements Bytes {
}
}

export class ClientMethodOptions extends External implements ClientMethodOptions {
constructor(crate: Crate, lifetime: Lifetime) {
super(crate, 'ClientMethodOptions', 'azure_core::http');
this.kind = 'clientMethodOptions';
this.lifetime = lifetime;
}
}

export class Decimal extends External implements Decimal {
constructor(crate: Crate, stringEncoding: boolean) {
super(crate, 'Decimal', 'rust_decimal', !stringEncoding ? ['serde-with-float'] : undefined);
Expand Down Expand Up @@ -814,10 +855,20 @@ export class Option<T> implements Option<T> {
}

export class Pager extends External implements Pager {
constructor(crate: Crate, type: Response<Model, Exclude<PayloadFormatType, 'NoFormat'>>) {
constructor(crate: Crate, type: Response<Model, Exclude<PayloadFormatType, 'NoFormat'>>, continuation: PagerContinuationKind) {
super(crate, 'Pager', 'azure_core::http');
this.kind = 'pager';
this.type = type;
this.continuation = continuation;
}
}

export class PagerOptions extends External implements PagerOptions {
constructor(crate: Crate, lifetime: Lifetime, continuation: PagerContinuationKind) {
super(crate, 'PagerOptions', 'azure_core::http::pager');
this.kind = 'pagerOptions';
this.lifetime = lifetime;
this.continuation = continuation;
}
}

Expand All @@ -829,6 +880,14 @@ export class Poller extends External implements Poller {
}
}

export class PollerOptions extends External implements PollerOptions {
constructor(crate: Crate, lifetime: Lifetime) {
super(crate, 'PollerOptions', 'azure_core::http::poller');
this.kind = 'pollerOptions';
this.lifetime = lifetime;
}
}

export class Payload<T> implements Payload<T> {
constructor(type: T, format: PayloadFormat) {
this.kind = 'payload';
Expand Down
21 changes: 15 additions & 6 deletions packages/typespec-rust/src/tcgcadapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1309,19 +1309,19 @@ export class Adapter {
methodOptionsStruct.lifetime = optionsLifetime;
methodOptionsStruct.docs.summary = `Options to be passed to ${this.asDocLink(`${rustClient.name}::${methodName}()`, `crate::generated::clients::${rustClient.name}::${methodName}()`)}`;

let clientMethodOptions: rust.ExternalType;
let clientMethodOptions: rust.ClientMethodOptions | rust.PagerOptions | rust.PollerOptions;
switch (method.kind) {
case 'paging':
clientMethodOptions = new rust.ExternalType(this.crate, 'PagerOptions', 'azure_core::http::pager');
// default to nextLink. will update it as required when we have that info
clientMethodOptions = new rust.PagerOptions(this.crate, optionsLifetime, 'nextLink');
break;
case 'lro':
clientMethodOptions = new rust.ExternalType(this.crate, 'PollerOptions', 'azure_core::http::poller');
clientMethodOptions = new rust.PollerOptions(this.crate, optionsLifetime);
break;
default:
clientMethodOptions = new rust.ExternalType(this.crate, 'ClientMethodOptions', 'azure_core::http');
clientMethodOptions = new rust.ClientMethodOptions(this.crate, optionsLifetime);
}

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

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

Expand Down Expand Up @@ -1689,6 +1690,14 @@ export class Adapter {
pageableMethod.strategy = this.adaptPageableMethodStrategy(method, paramsMap, responseHeadersMap);
if (pageableMethod.strategy?.kind === 'nextLink') {
pageableMethod.strategy.reinjectedParams = this.adaptPageableMethodReinjectionParams(method, paramsMap);
} else if (pageableMethod.strategy?.kind === 'continuationToken') {
// set the continuation type to token on the Pager and the PagerOptions field in the method options
pageableMethod.returns.type.continuation = 'token';
for (const field of pageableMethod.options.type.fields) {
if (field.type.kind === 'pagerOptions') {
field.type.continuation = 'token';
}
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-rust/test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ rust-version = "1.85"
[workspace.dependencies]
# Third-party dependencies should be kept up to date with https://github.com/Azure/azure-sdk-for-rust/blob/main/Cargo.lock
async-trait = "0.1.88"
azure_core = { git = "https://github.com/Azure/azure-sdk-for-rust.git", rev = "1bc0ef3a9e27af4990435de23e1127155fbba68c", features = [
azure_core = { git = "https://github.com/Azure/azure-sdk-for-rust.git", rev = "197ddcf43752a5d2b28ae77cb7433f2a6ebcb57e", features = [
"decimal",
"reqwest",
] }
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading