Skip to content
Open
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"
---

Allow `@override` to replace a client method response and add the `replaceResponse` customization function.
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,26 @@ export type ReorderParametersFunctionImplementation = (
order: readonly string[],
) => Operation;

/**
* Replace the method response type of an operation.
* This preserves the HTTP response metadata and only changes the return type
* of the generated client method when used with `@@override`.
*
* @param operation The operation to transform.
* @param response The replacement method response type.
* @returns A new operation with the response type replaced.
* @example Replace a response with void
* ```typespec
* alias DeleteResponse = replaceResponse(MyService.delete, void);
* @@override(MyService.delete, DeleteResponse);
* ```
*/
export type ReplaceResponseFunctionImplementation = (
context: FunctionContext,
operation: Operation,
response: Type,
) => Operation;

/**
* Mark a client name as exact, preventing language emitters from applying
* their usual casing transformations (e.g., snake_case for Python, camelCase for JavaScript).
Expand Down Expand Up @@ -1286,5 +1306,6 @@ export type AzureClientGeneratorCoreFunctions = {
removeParameter: RemoveParameterFunctionImplementation;
addParameter: AddParameterFunctionImplementation;
reorderParameters: ReorderParametersFunctionImplementation;
replaceResponse: ReplaceResponseFunctionImplementation;
exact: ExactFunctionImplementation;
};
18 changes: 18 additions & 0 deletions packages/typespec-client-generator-core/lib/functions.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@ extern fn reorderParameters(
order: valueof string[]
): Reflection.Operation;

/**
* Replace the method response type of an operation.
* This preserves the HTTP response metadata and only changes the return type
* of the generated client method when used with `@@override`.
*
* @param operation The operation to transform.
* @param response The replacement method response type.
* @returns A new operation with the response type replaced.
*
* @example Replace a response with void
* ```typespec
* alias DeleteResponse = replaceResponse(MyService.delete, void);
* @@override(MyService.delete, DeleteResponse);
* ```
*/
#suppress "experimental-feature" "replaceResponse uses extern fn which is experimental but provides essential response transformation functionality"
extern fn replaceResponse(operation: Reflection.Operation, response: unknown): Reflection.Operation;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When we replace the response, how could we know how to map the method level response to the HTTP level response?


/**
* Mark a client name as exact, preventing language emitters from applying
* their usual casing transformations (e.g., snake_case for Python, camelCase for JavaScript).
Expand Down
26 changes: 25 additions & 1 deletion packages/typespec-client-generator-core/src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,27 @@ export function reorderParameters(
return cloneOperation(tk, operation, { parameters: newProperties });
}

/**
* Replace the method response type of an operation.
*
* The operation's HTTP response metadata is preserved; only the client method
* return type is changed when the operation is used with `@override`.
*
* @param context The function context provided by TypeSpec
* @param operation The operation to transform
* @param response The replacement method response type
* @returns A new operation with the response type replaced
*/
export function replaceResponse(
context: FunctionContext,
operation: Operation,
response: Type,
): Operation {
return cloneOperation($(context.program), operation, {
returnType: response,
});
}

/**
* Mark a client name as exact, preventing language emitters from applying
* their usual casing transformations.
Expand Down Expand Up @@ -269,7 +290,10 @@ export function hasExactNameMarker(name: string): boolean {
* @param name The name to normalize
* @returns An object with the clean name and whether it was marked as exact
*/
export function normalizeExactName(name: string): { name: string; isExactName: boolean } {
export function normalizeExactName(name: string): {
name: string;
isExactName: boolean;
} {
if (name.startsWith(EXACT_NAME_PREFIX)) {
return { name: name.slice(EXACT_NAME_PREFIX.length), isExactName: true };
}
Expand Down
33 changes: 26 additions & 7 deletions packages/typespec-client-generator-core/src/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ function getSdkPagingServiceMethod<TServiceOperation extends SdkServiceOperation
const diagnostics = createDiagnosticCollector();

const baseServiceMethod = diagnostics.pipe(
getSdkBasicServiceMethod<TServiceOperation>(context, operation, client),
getSdkBasicServiceMethod<TServiceOperation>(context, operation, client, false),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why here we pass false? This flag looks quick hacky to me.

);

// If the response body type itself is nullable (e.g., {@body body: Type | null}), unwrap it for paging/LRO processing
Expand Down Expand Up @@ -327,7 +327,10 @@ export function getPropertySegmentsFromModelOrParameters(
source: SdkModelType | SdkMethodParameter[],
predicate: (property: SdkMethodParameter | SdkModelPropertyType) => boolean,
): (SdkMethodParameter | SdkModelPropertyType)[] | undefined {
const queue: { model: SdkModelType; path: (SdkMethodParameter | SdkModelPropertyType)[] }[] = [];
const queue: {
model: SdkModelType;
path: (SdkMethodParameter | SdkModelPropertyType)[];
}[] = [];

if (!Array.isArray(source)) {
if (source.baseModel) {
Expand Down Expand Up @@ -607,8 +610,12 @@ function getSdkMethodResponse(
operation: Operation,
sdkOperation: SdkServiceOperation,
client: SdkClientType<SdkServiceOperation>,
useResponseOverride = true,
): SdkMethodResponse {
const responses = sdkOperation.responses;
const responseOverride = useResponseOverride
? getOverriddenClientMethod(context, operation)?.returnType
: undefined;

const allResponseBodies: SdkType[] = [];
let containsResponseWithoutBody = false;
Expand All @@ -622,7 +629,11 @@ function getSdkMethodResponse(

const responseTypes = new Set<string>(allResponseBodies.map((x) => getHashForType(x)));
let type: SdkType | undefined = undefined;
if (getResponseAsBool(context, operation)) {
if (responseOverride && isNeverOrVoidType(responseOverride)) {
type = undefined;
} else if (responseOverride) {
type = ignoreDiagnostics(getClientTypeWithDiagnostics(context, responseOverride, operation));
} else if (getResponseAsBool(context, operation)) {
type = getSdkBuiltInType(context, $(context.program).builtin.boolean);
} else {
if (responseTypes.size > 1) {
Expand Down Expand Up @@ -678,6 +689,7 @@ export function getSdkBasicServiceMethod<TServiceOperation extends SdkServiceOpe
context: TCGCContext,
operation: Operation,
client: SdkClientType<TServiceOperation>,
useResponseOverride = true,
): [SdkServiceMethod<TServiceOperation>, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
const methodParameters: SdkMethodParameter[] = [];
Expand Down Expand Up @@ -722,7 +734,13 @@ export function getSdkBasicServiceMethod<TServiceOperation extends SdkServiceOpe
const serviceOperation = diagnostics.pipe(
getSdkServiceOperation<TServiceOperation>(context, operation, methodParameters, client),
);
const response = getSdkMethodResponse(context, operation, serviceOperation, client);
const response = getSdkMethodResponse(
context,
operation,
serviceOperation,
client,
useResponseOverride,
);
const name = getLibraryName(context, operation);
return diagnostics.wrap({
__raw: operation,
Expand All @@ -749,12 +767,13 @@ function getSdkServiceMethod<TServiceOperation extends SdkServiceOperation>(
operation: Operation,
client: SdkClientType<TServiceOperation>,
): [SdkServiceMethod<TServiceOperation>, readonly Diagnostic[]] {
const lro = getTcgcLroMetadata(context, operation, client);
const clientOperation = getOverriddenClientMethod(context, operation) ?? operation;
const lro = getTcgcLroMetadata(context, clientOperation, client);
// `@disablePageable` disables paging even for operations with @list
const pagingDisabled = getDisablePageable(context, operation);
const pagingDisabled = getDisablePageable(context, clientOperation);
const paging =
!pagingDisabled &&
(isList(context.program, operation) || getMarkAsPageable(context, operation));
(isList(context.program, clientOperation) || getMarkAsPageable(context, clientOperation));
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 @@ -39,6 +39,7 @@ import {
removeParameter,
reorderParameters,
replaceParameter,
replaceResponse,
} from "./functions.js";

export { $lib } from "./lib.js";
Expand Down Expand Up @@ -91,6 +92,7 @@ export const $functions: Record<string, AzureClientGeneratorCoreFunctions> = {
removeParameter: removeParameter as AzureClientGeneratorCoreFunctions["removeParameter"],
addParameter: addParameter as AzureClientGeneratorCoreFunctions["addParameter"],
reorderParameters: reorderParameters as AzureClientGeneratorCoreFunctions["reorderParameters"],
replaceResponse: replaceResponse as AzureClientGeneratorCoreFunctions["replaceResponse"],
exact: exact as AzureClientGeneratorCoreFunctions["exact"],
},
};
Loading
Loading