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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

Add new `@Legacy.disablePageable` decorator to mark pageable operations as basic operations
42 changes: 42 additions & 0 deletions packages/typespec-client-generator-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,7 @@ model MyModel {
### Azure.ClientGenerator.Core.Legacy

- [`@clientDefaultValue`](#@clientdefaultvalue)
- [`@disablePageable`](#@disablepageable)
- [`@flattenProperty`](#@flattenproperty)
- [`@hierarchyBuilding`](#@hierarchybuilding)
- [`@markAsLro`](#@markaslro)
Expand Down Expand Up @@ -1345,6 +1346,47 @@ model Config {
}
```

#### `@disablePageable`

Prevents an operation from being treated as a pageable operation by the SDK generators,
even when the operation follows standard paging patterns (e.g., decorated with `@list`).

When applied, the operation will be treated as a basic method:

- The response will be the paged model itself (not the list of items)
- The paged model will not be marked with paged result usage
- No paging mechanisms (iterators/async iterators) will be generated

This decorator is considered legacy functionality and should only be used when
you need to override the default paging behavior for specific operations.

```typespec
@Azure.ClientGenerator.Core.Legacy.disablePageable(scope?: valueof string)
```

##### Target

The operation that should NOT be treated as a pageable operation
`Operation`

##### Parameters

| Name | Type | Description |
| ----- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| scope | `valueof string` | Specifies the target language emitters that the decorator should apply.<br />If not set, the decorator will be applied to all language emitters by default.<br />You can use "!" to exclude specific languages, for example: !(java, python) or !java, !python. |

##### Examples

###### Prevent a paging operation from being treated as pageable

```typespec
@Azure.ClientGenerator.Core.Legacy.disablePageable
@list
@route("/items")
@get
op listItems(): ItemListResult;
```

#### `@flattenProperty`

Set whether a model property should be flattened or not.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,37 @@ export type MarkAsPageableDecorator = (
scope?: string,
) => DecoratorValidatorCallbacks | void;

/**
* Prevents an operation from being treated as a pageable operation by the SDK generators,
* even when the operation follows standard paging patterns (e.g., decorated with `@list`).
*
* When applied, the operation will be treated as a basic method:
* - The response will be the paged model itself (not the list of items)
* - The paged model will not be marked with paged result usage
* - No paging mechanisms (iterators/async iterators) will be generated
*
* This decorator is considered legacy functionality and should only be used when
* you need to override the default paging behavior for specific operations.
*
* @param target The operation that should NOT be treated as a pageable operation
* @param scope Specifies the target language emitters that the decorator should apply.
* If not set, the decorator will be applied to all language emitters by default.
* You can use "!" to exclude specific languages, for example: !(java, python) or !java, !python.
* @example Prevent a paging operation from being treated as pageable
* ```typespec
* @Azure.ClientGenerator.Core.Legacy.disablePageable
* @list
* @route("/items")
* @get
* op listItems(): ItemListResult;
* ```
*/
export type DisablePageableDecorator = (
context: DecoratorContext,
target: Operation,
scope?: string,
) => DecoratorValidatorCallbacks | void;

/**
* Specifies the HTTP verb for the next link operation in a paging scenario.
*
Expand Down Expand Up @@ -229,6 +260,7 @@ export type AzureClientGeneratorCoreLegacyDecorators = {
flattenProperty: FlattenPropertyDecorator;
markAsLro: MarkAsLroDecorator;
markAsPageable: MarkAsPageableDecorator;
disablePageable: DisablePageableDecorator;
nextLinkVerb: NextLinkVerbDecorator;
clientDefaultValue: ClientDefaultValueDecorator;
};
29 changes: 29 additions & 0 deletions packages/typespec-client-generator-core/lib/legacy.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,35 @@ extern dec markAsLro(target: Operation, scope?: valueof string);
*/
extern dec markAsPageable(target: Operation, scope?: valueof string);

/**
* Prevents an operation from being treated as a pageable operation by the SDK generators,
* even when the operation follows standard paging patterns (e.g., decorated with `@list`).
*
* When applied, the operation will be treated as a basic method:
* - The response will be the paged model itself (not the list of items)
* - The paged model will not be marked with paged result usage
* - No paging mechanisms (iterators/async iterators) will be generated
*
* This decorator is considered legacy functionality and should only be used when
* you need to override the default paging behavior for specific operations.
*
* @param target The operation that should NOT be treated as a pageable operation
* @param scope Specifies the target language emitters that the decorator should apply.
* If not set, the decorator will be applied to all language emitters by default.
* You can use "!" to exclude specific languages, for example: !(java, python) or !java, !python.
*
* @example Prevent a paging operation from being treated as pageable
* ```typespec
* @Azure.ClientGenerator.Core.Legacy.disablePageable
* @list
* @route("/items")
* @get
* op listItems(): ItemListResult;
* ```
*
*/
extern dec disablePageable(target: Operation, scope?: valueof string);

/**
* Specifies the HTTP verb for the next link operation in a paging scenario.
*
Expand Down
15 changes: 15 additions & 0 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
} from "../generated-defs/Azure.ClientGenerator.Core.js";
import {
ClientDefaultValueDecorator,
DisablePageableDecorator,
FlattenPropertyDecorator,
HierarchyBuildingDecorator,
MarkAsLroDecorator,
Expand Down Expand Up @@ -1747,6 +1748,20 @@ export interface MarkAsPageableInfo {
itemsProperty: ModelProperty;
}

const disablePageableKey = createStateSymbol("disablePageable");

export const $disablePageable: DisablePageableDecorator = (
context: DecoratorContext,
target: Operation,
scope?: LanguageScopes,
) => {
setScopedDecoratorData(context, $disablePageable, disablePageableKey, target, true, scope);
};

export function getDisablePageable(context: TCGCContext, entity: Operation): boolean {
return getScopedDecoratorData(context, disablePageableKey, entity) ?? false;
}

function getRealResponseModel(program: Program, responseModel: Model): Type {
let bodyProperty: ModelProperty | undefined = undefined;
for (const prop of responseModel.properties.values()) {
Expand Down
7 changes: 6 additions & 1 deletion packages/typespec-client-generator-core/src/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { $ } from "@typespec/compiler/typekit";
import {
getAccess,
getDisablePageable,
getMarkAsPageable,
getNextLinkVerb,
getOverriddenClientMethod,
Expand Down Expand Up @@ -735,7 +736,11 @@ function getSdkServiceMethod<TServiceOperation extends SdkServiceOperation>(
client: SdkClientType<TServiceOperation>,
): [SdkServiceMethod<TServiceOperation>, readonly Diagnostic[]] {
const lro = getTcgcLroMetadata(context, operation, client);
const paging = isList(context.program, operation) || getMarkAsPageable(context, operation);
// `@disablePageable` disables paging even for operations with @list
const pagingDisabled = getDisablePageable(context, operation);
const paging =
!pagingDisabled &&
(isList(context.program, operation) || getMarkAsPageable(context, operation));
if (lro && paging) {
return getSdkLroPagingServiceMethod<TServiceOperation>(context, operation, client);
} else if (paging) {
Expand Down
2 changes: 2 additions & 0 deletions packages/typespec-client-generator-core/src/tsp-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
$clientNamespace,
$convenientAPI,
$deserializeEmptyStringAsNull,
$disablePageable,
$flattenProperty,
$legacyHierarchyBuilding,
$markAsLro,
Expand Down Expand Up @@ -62,6 +63,7 @@ export const $decorators = {
flattenProperty: $flattenProperty,
markAsLro: $markAsLro,
markAsPageable: $markAsPageable,
disablePageable: $disablePageable,
nextLinkVerb: $nextLinkVerb,
clientDefaultValue: $clientDefaultValue,
} satisfies AzureClientGeneratorCoreLegacyDecorators,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { ok, strictEqual } from "assert";
import { beforeEach, it } from "vitest";
import { SdkTestRunner, createSdkTestRunner } from "../test-host.js";

let basicRunner: SdkTestRunner;

beforeEach(async () => {
basicRunner = await createSdkTestRunner({
emitterName: "@azure-typespec/http-client-csharp",
});
});

it("should disable paging when @disablePageable is applied to a @list operation", async () => {
await basicRunner.compile(`
@service
namespace TestService {
model ItemListResult {
@pageItems
items: Item[];
nextLink?: string;
}

model Item {
id: string;
name: string;
}

@Azure.ClientGenerator.Core.Legacy.disablePageable
@list
@route("/items")
@get
op listItems(): ItemListResult;
}
`);

const methods = basicRunner.context.sdkPackage.clients[0].methods;
strictEqual(methods.length, 1);

const method = methods[0];
// Should be basic method since @disablePageable disables paging
strictEqual(method.kind, "basic");
strictEqual(method.name, "listItems");

// Response should be the paged model itself, not the list of items
const responseType = method.response.type;
ok(responseType);
strictEqual(responseType.kind, "model");
strictEqual(responseType.name, "ItemListResult");

// Check that resultSegments is not populated (not a paging method)
strictEqual(method.response.resultSegments, undefined);
});

it("should disable paging with language scope when @disablePageable(scope) is applied", async () => {
await basicRunner.compile(`
@service
namespace TestService {
model ItemListResult {
@pageItems
items: Item[];
nextLink?: string;
}

model Item {
id: string;
name: string;
}

@Azure.ClientGenerator.Core.Legacy.disablePageable("csharp")
@list
@route("/items")
@get
op listItems(): ItemListResult;
}
`);

const methods = basicRunner.context.sdkPackage.clients[0].methods;
strictEqual(methods.length, 1);

const method = methods[0];
// Should be basic method since @disablePageable disables paging for csharp
strictEqual(method.kind, "basic");
strictEqual(method.name, "listItems");

// Response should be the paged model itself
const responseType = method.response.type;
ok(responseType);
strictEqual(responseType.kind, "model");
strictEqual(responseType.name, "ItemListResult");
});

it("should NOT disable paging when scope does not match for @disablePageable", async () => {
await basicRunner.compile(`
@service
namespace TestService {
model ItemListResult {
@pageItems
items: Item[];
nextLink?: string;
}

model Item {
id: string;
name: string;
}

@Azure.ClientGenerator.Core.Legacy.disablePageable("python")
@list
@route("/items")
@get
op listItems(): ItemListResult;
}
`);

const methods = basicRunner.context.sdkPackage.clients[0].methods;
strictEqual(methods.length, 1);

const method = methods[0];
// Should still be paging method since scope is python but emitter is csharp
strictEqual(method.kind, "paging");
strictEqual(method.name, "listItems");

// Response should be the list of items (paging behavior)
const responseType = method.response.type;
ok(responseType);
strictEqual(responseType.kind, "array");
});

it("should not mark paged model as paged result when @disablePageable is applied", async () => {
const { isPagedResultModel } = await import("../../src/public-utils.js");

await basicRunner.compile(`
@service
namespace TestService {
model ItemListResult {
@pageItems
items: Item[];
nextLink?: string;
}

model Item {
id: string;
name: string;
}

@Azure.ClientGenerator.Core.Legacy.disablePageable
@list
@route("/items")
@get
op listItems(): ItemListResult;
}
`);

const methods = basicRunner.context.sdkPackage.clients[0].methods;
strictEqual(methods.length, 1);

const method = methods[0];
strictEqual(method.kind, "basic");

// The response type should NOT be marked as a paged result model
const responseType = method.response.type;
ok(responseType);
strictEqual(responseType.kind, "model");
strictEqual(isPagedResultModel(basicRunner.context, responseType), false);
});
Loading
Loading