Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 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
Expand Up @@ -113,6 +113,40 @@ export type MarkAsLroDecorator = (
scope?: string,
) => void;

/**
* Forces an operation to be treated as a pageable operation by the SDK generators,
* even when the operation does not follow standard paging patterns on the service side.
*
* NOTE: When used, you will need to verify the operation and add tests for the generated code
* to make sure the end-to-end works for library users, since there is a risk that forcing
* this operation to be pageable will result in errors.
*
* When applied, TCGC will treat the operation as pageable and SDK generators should:
* - Generate paging mechanisms (iterators/async iterators)
* - Return appropriate pageable-specific return types
* - Handle the operation as a collection that may require multiple requests
*
* This decorator is considered legacy functionality and should only be used when
* standard TypeSpec paging patterns are not feasible.
*
* @param target The operation that should 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 Force a regular operation to be treated as pageable for backward compatibility
* ```typespec
* @Azure.ClientGenerator.Core.Legacy.markAsPageable
* @route("/items")
* @get
* op listItems(): ItemListResult;
* ```
*/
export type MarkAsPageableDecorator = (
context: DecoratorContext,
target: Operation,
scope?: string,
) => void;

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

/**
* Forces an operation to be treated as a pageable operation by the SDK generators,
* even when the operation does not follow standard paging patterns on the service side.
*
* NOTE: When used, you will need to verify the operation and add tests for the generated code
* to make sure the end-to-end works for library users, since there is a risk that forcing
* this operation to be pageable will result in errors.
*
* When applied, TCGC will treat the operation as pageable and SDK generators should:
* - Generate paging mechanisms (iterators/async iterators)
* - Return appropriate pageable-specific return types
* - Handle the operation as a collection that may require multiple requests
*
* This decorator is considered legacy functionality and should only be used when
* standard TypeSpec paging patterns are not feasible.
*
* @param target The operation that should 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 Force a regular operation to be treated as pageable for backward compatibility
* ```typespec
* @Azure.ClientGenerator.Core.Legacy.markAsPageable
* @route("/items")
* @get
* op listItems(): ItemListResult;
* ```
*
*/
extern dec markAsPageable(target: Operation, scope?: valueof string);

/**
* Specifies the HTTP verb for the next link operation in a paging scenario.
*
Expand Down
83 changes: 83 additions & 0 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getLroMetadata } from "@azure-tools/typespec-azure-core";
import {
$decorators as typespecDecorators,
compilerAssert,
DecoratorContext,
DecoratorFunction,
Expand All @@ -10,6 +11,7 @@ import {
ignoreDiagnostics,
Interface,
isErrorModel,
isList,
isNumeric,
isService,
isTemplateDeclaration,
Expand Down Expand Up @@ -51,6 +53,7 @@ import {
FlattenPropertyDecorator,
HierarchyBuildingDecorator,
MarkAsLroDecorator,
MarkAsPageableDecorator,
NextLinkVerbDecorator,
} from "../generated-defs/Azure.ClientGenerator.Core.Legacy.js";
import {
Expand Down Expand Up @@ -1535,6 +1538,86 @@ export function getMarkAsLro(context: TCGCContext, entity: Operation): boolean {
return getScopedDecoratorData(context, markAsLroKey, entity) ?? false;
}

const markAsPageableKey = createStateSymbol("markAsPageable");

export const $markAsPageable: MarkAsPageableDecorator = (
context: DecoratorContext,
target: Operation,
scope?: LanguageScopes,
) => {
const httpOperation = ignoreDiagnostics(getHttpOperation(context.program, target));
const hasModelResponse = httpOperation.responses.filter(
(r) =>
r.type?.kind === "Model" && !(r.statusCodes === "*" || isErrorModel(context.program, r.type)),
)[0];
if (!hasModelResponse || hasModelResponse.type?.kind !== "Model") {
reportDiagnostic(context.program, {
code: "invalid-mark-as-pageable-target",
format: {
operation: target.name,
},
target: context.decoratorTarget,
});
return;
}

// Check if already marked with @list decorator
if (isList(context.program, target)) {
reportDiagnostic(context.program, {
code: "mark-as-pageable-ineffective",
format: {
operation: target.name,
},
target: context.decoratorTarget,
});
return;
}

// Check the response model for @pageItems decorator
const responseModel = hasModelResponse.type as Model;
let hasPageItemsProperty = false;

// Check if any property has @pageItems decorator
for (const [, prop] of responseModel.properties) {
// Check if the property is marked with @pageItems by checking the program state
// The @pageItems decorator uses a state symbol "pageItems"
const pageItemsStateKey = Symbol.for("TypeSpec.pageItems");
if (context.program.stateSet(pageItemsStateKey).has(prop)) {
hasPageItemsProperty = true;
break;
Copy link
Member

Choose a reason for hiding this comment

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

to check if a property has this decorator, we should use this function from typespec/compiler: isPageItemsProperty

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to use isPageItemsProperty function from @typespec/compiler instead of manually checking the program state. Changes made in commit 0e21505.

}
}

if (!hasPageItemsProperty) {
// Try to find a property named "value" and apply @pageItems to it
const valueProperty = responseModel.properties.get("value");
if (valueProperty) {
// Apply @pageItems decorator to the value property
context.call(typespecDecorators.TypeSpec.pageItems, valueProperty);
} else {
// No @pageItems property and no "value" property found
reportDiagnostic(context.program, {
code: "invalid-mark-as-pageable-target",
format: {
operation: target.name,
},
target: context.decoratorTarget,
});
return;
}
}

// Apply the @list decorator to the operation
context.call(typespecDecorators.TypeSpec.list, target);

// Store metadata that will be checked by TCGC to treat this operation as pageable
setScopedDecoratorData(context, $markAsPageable, markAsPageableKey, target, true, scope);
};

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

const nextLinkVerbKey = createStateSymbol("nextLinkVerb");

export const $nextLinkVerb: NextLinkVerbDecorator = (
Expand Down
12 changes: 12 additions & 0 deletions packages/typespec-client-generator-core/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,18 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`@markAsLro decorator is ineffective since this operation already returns real LRO metadata. Please remove the @markAsLro decorator.`,
},
},
"invalid-mark-as-pageable-target": {
severity: "warning",
messages: {
default: paramMessage`@markAsPageable decorator can only be applied to operations that return a model with a property decorated with @pageItems or a property named 'value'. We will ignore this decorator.`,
},
},
"mark-as-pageable-ineffective": {
severity: "warning",
messages: {
default: paramMessage`@markAsPageable decorator is ineffective since this operation is already marked as pageable with @list decorator. Please remove the @markAsPageable decorator.`,
},
},
"api-version-undefined": {
severity: "warning",
messages: {
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 @@ -17,6 +17,7 @@ import {
$flattenProperty,
$legacyHierarchyBuilding,
$markAsLro,
$markAsPageable,
$nextLinkVerb,
$operationGroup,
$override,
Expand Down Expand Up @@ -60,6 +61,7 @@ export const $decorators = {
hierarchyBuilding: $legacyHierarchyBuilding,
flattenProperty: $flattenProperty,
markAsLro: $markAsLro,
markAsPageable: $markAsPageable,
nextLinkVerb: $nextLinkVerb,
clientDefaultValue: $clientDefaultValue,
} satisfies AzureClientGeneratorCoreLegacyDecorators,
Expand Down
Loading
Loading