diff --git a/packages/typespec-ts/src/lib.ts b/packages/typespec-ts/src/lib.ts index 77903aebd4..86b94e7708 100644 --- a/packages/typespec-ts/src/lib.ts +++ b/packages/typespec-ts/src/lib.ts @@ -86,15 +86,6 @@ export interface EmitterOptions { * Set to false to return the non-model types directly. */ "wrap-non-model-return"?: boolean; - /** - * When set to true, HEAD operations with a void response body will return `{ body: boolean }` - * instead of `void`, where `body: true` indicates a 2xx success (resource exists) and - * `body: false` indicates a non-2xx response (e.g., 404 Not Found). - * This matches the HLC behavior for resource existence check operations. - * Only applies when `wrap-non-model-return` is also enabled. - * Defaults to `false`. - */ - "head-as-boolean"?: boolean; /** * When enabled, every regular (non-LRO, non-paging) operation return type is augmented with a * `_response` property containing `rawResponse`, `parsedBody`, and `parsedHeaders`. @@ -405,12 +396,6 @@ export const RLCOptionsSchema: JSONSchemaType = { description: "When set to true (default for Azure services), non-model return types (arrays, scalars, enums, bytes with binary content type) will be wrapped in an XxxResponse type for HLC backward compatibility during TypeSpec migration." }, - "head-as-boolean": { - type: "boolean", - nullable: true, - description: - "When set to true, HEAD operations with void response return `{ body: boolean }` (true=2xx, false=non-2xx) instead of void. Requires wrap-non-model-return to also be enabled. Defaults to false." - }, "enable-storage-compat": { type: "boolean", nullable: true, diff --git a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts index f96c4edca3..aae35ed0e2 100644 --- a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts @@ -234,7 +234,7 @@ export function getDeserializePrivateFunction( name: (response as any).name ?? "", type: getTypeExpression(context, response.type) }; - } else if (isHeadAsBooleanOperation(context, operation)) { + } else if (isHeadAsBooleanOperation(operation)) { returnType = { name: "", type: "boolean" }; } else { returnType = { name: "", type: "void" }; @@ -413,7 +413,7 @@ export function getDeserializePrivateFunction( statements.push( `return { blobBody: result.blobBody, readableStreamBody: result.readableStreamBody };` ); - } else if (isHeadAsBooleanOperation(context, operation)) { + } else if (isHeadAsBooleanOperation(operation)) { // HEAD has no body; derive boolean from status code statements.push(`return { body: result.status.startsWith("2") };`); } else { @@ -442,7 +442,7 @@ export function getDeserializePrivateFunction( isAzureCoreErrorType(context.program, deserializedType.__raw) ) { statements.push(`return ${deserializedRoot}${multipartCastSuffix}`); - } else if (isHeadAsBooleanOperation(context, operation)) { + } else if (isHeadAsBooleanOperation(operation)) { // HEAD has no body; derive boolean from status code statements.push(`return result.status.startsWith("2");`); } else { @@ -459,7 +459,7 @@ export function getDeserializePrivateFunction( ); } } - } else if (isHeadAsBooleanOperation(context, operation)) { + } else if (isHeadAsBooleanOperation(operation)) { if (shouldWrap) { statements.push(`return { body: result.status.startsWith("2") };`); } else { @@ -1053,7 +1053,7 @@ export function getOperationFunction( name: "", type: `${buildHeaderOnlyResponseType(context, responseHeaders)}` }; - } else if (isHeadAsBooleanOperation(context, operation)) { + } else if (isHeadAsBooleanOperation(operation)) { returnType = { name: "", type: "boolean" }; } @@ -2983,10 +2983,10 @@ export function getExpectedStatuses( context?: SdkContext ): string { let statusCodes = operation.operation.responses.map((x) => x.statusCodes); - // For HEAD + @responseAsBool / head-as-boolean, 404 is a valid "false" response. + // For HEAD + @responseAsBool, 404 is a valid "false" response. if ( context && - isHeadAsBooleanOperation(context, operation) && + isHeadAsBooleanOperation(operation) && !statusCodes.includes(404) ) { statusCodes = [...statusCodes, 404]; @@ -3209,15 +3209,11 @@ function isHeadOperation(operation: ServiceOperation): boolean { return operation.operation.verb.toLowerCase() === "head"; } -function isHeadAsBooleanOperation( - context: SdkContext, - operation: ServiceOperation -): boolean { +function isHeadAsBooleanOperation(operation: ServiceOperation): boolean { if (!isHeadOperation(operation)) return false; // @responseAsBool: TCGC promotes response.type to SdkBuiltInType { kind: "boolean" } if ((operation.response.type as any)?.kind === "boolean") return true; - // Legacy head-as-boolean emitter option (response.type remains void) - return !!context.rlcOptions?.headAsBoolean; + return false; } /** @@ -3254,9 +3250,8 @@ export function checkWrapNonModelReturn( const { type } = operation.response; if (!type) { - // Special case: HEAD operation with void response → wrap as boolean { body: boolean } - // Triggered by head-as-boolean emitter option. - if (isHeadAsBooleanOperation(context, operation)) { + // Special case: HEAD operation with @responseAsBool and void response → wrap as boolean { body: boolean } + if (isHeadAsBooleanOperation(operation)) { return { shouldWrap: true, isBinary: false }; } return noWrap; // void return type - no wrap needed @@ -3307,10 +3302,6 @@ export function buildNonModelResponseTypeDeclaration( */ readableStreamBody?: ${nodeReadableStreamRef}; }`; - } else if (!operation.response.type && isHeadOperation(operation)) { - // HEAD as boolean: the body property is a boolean indicating if the resource exists. - // true = resource exists (2xx response), false = resource not found (e.g., 404) - typeBody = `{ body: boolean }`; } else { const returnType = getTypeExpression(context, operation.response.type!); typeBody = `{ body: ${returnType} }`; diff --git a/packages/typespec-ts/src/transform/transfromRLCOptions.ts b/packages/typespec-ts/src/transform/transfromRLCOptions.ts index 7c16292adc..17acec148e 100644 --- a/packages/typespec-ts/src/transform/transfromRLCOptions.ts +++ b/packages/typespec-ts/src/transform/transfromRLCOptions.ts @@ -95,7 +95,6 @@ function extractRLCOptions( const enableStorageCompat = emitterOptions["enable-storage-compat"] === true; const treatUnknownAsRecord = emitterOptions["treat-unknown-as-record"] === true; - const headAsBoolean = emitterOptions["head-as-boolean"] === true; const typespecTitleMap = emitterOptions["typespec-title-map"]; const generateReactNativeTarget = emitterOptions["generate-react-native-target"] === true; @@ -145,7 +144,6 @@ function extractRLCOptions( isMultiService, enableStorageCompat, treatUnknownAsRecord, - headAsBoolean, generateReactNativeTarget }; } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/wrapNonModelReturn.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/wrapNonModelReturn.md index bd26f0c1dc..7f4e10b36c 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/operations/wrapNonModelReturn.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/wrapNonModelReturn.md @@ -948,230 +948,6 @@ export function getIkeSas( } ``` -# wrap-non-model-return wraps HEAD void response as boolean body property - -When both `wrap-non-model-return` and `head-as-boolean` are enabled and the operation is a HEAD request with no response body, -the response is wrapped as `{ body: boolean }` to indicate resource existence (true = exists, false = not found). -This matches HLC behavior for HEAD operations. - -## TypeSpec - -```tsp -@route("/resource-groups/{resourceGroupName}") -@head -op checkExistence(@path resourceGroupName: string): { - @statusCode _: 204; -} | { - @statusCode _: 404; -}; -``` - -```yaml -wrap-non-model-return: true -head-as-boolean: true -``` - -## Models - -```ts models alias CheckExistenceResponse -export type CheckExistenceResponse = { body: boolean }; -``` - -## Operations - -```ts operations -import { TestingContext as Client } from "./index.js"; -import { CheckExistenceResponse } from "../models/models.js"; -import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; -import { CheckExistenceOptionalParams } from "./options.js"; -import { - StreamableMethod, - PathUncheckedResponse, - createRestError, - operationOptionsToRequestParameters, -} from "@azure-rest/core-client"; - -export function _checkExistenceSend( - context: Client, - resourceGroupName: string, - options: CheckExistenceOptionalParams = { requestOptions: {} }, -): StreamableMethod { - const path = expandUrlTemplate( - "/resource-groups/{resourceGroupName}", - { - resourceGroupName: resourceGroupName, - }, - { - allowReserved: options?.requestOptions?.skipUrlEncoding, - }, - ); - return context.path(path).head({ ...operationOptionsToRequestParameters(options) }); -} - -export async function _checkExistenceDeserialize( - result: PathUncheckedResponse, -): Promise { - const expectedStatuses = ["204", "404"]; - if (!expectedStatuses.includes(result.status)) { - throw createRestError(result); - } - - return { body: result.status.startsWith("2") }; -} - -export async function checkExistence( - context: Client, - resourceGroupName: string, - options: CheckExistenceOptionalParams = { requestOptions: {} }, -): Promise { - const result = await _checkExistenceSend(context, resourceGroupName, options); - return _checkExistenceDeserialize(result); -} -``` - -# wrap-non-model-return wraps HEAD void-only response as boolean body property - -When both `wrap-non-model-return` and `head-as-boolean` are enabled and the operation is a HEAD request returning only void (success only), -the response is wrapped as `{ body: boolean }`. - -## TypeSpec - -```tsp -@route("/resources/{resourceName}") -@head -op headResource(@path resourceName: string): void; -``` - -```yaml -wrap-non-model-return: true -head-as-boolean: true -``` - -## Models - -```ts models alias HeadResourceResponse -export type HeadResourceResponse = { body: boolean }; -``` - -## Operations - -```ts operations -import { TestingContext as Client } from "./index.js"; -import { HeadResourceResponse } from "../models/models.js"; -import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; -import { HeadResourceOptionalParams } from "./options.js"; -import { - StreamableMethod, - PathUncheckedResponse, - createRestError, - operationOptionsToRequestParameters, -} from "@azure-rest/core-client"; - -export function _headResourceSend( - context: Client, - resourceName: string, - options: HeadResourceOptionalParams = { requestOptions: {} }, -): StreamableMethod { - const path = expandUrlTemplate( - "/resources/{resourceName}", - { - resourceName: resourceName, - }, - { - allowReserved: options?.requestOptions?.skipUrlEncoding, - }, - ); - return context.path(path).head({ ...operationOptionsToRequestParameters(options) }); -} - -export async function _headResourceDeserialize( - result: PathUncheckedResponse, -): Promise { - const expectedStatuses = ["204", "404"]; - if (!expectedStatuses.includes(result.status)) { - throw createRestError(result); - } - - return { body: result.status.startsWith("2") }; -} - -export async function headResource( - context: Client, - resourceName: string, - options: HeadResourceOptionalParams = { requestOptions: {} }, -): Promise { - const result = await _headResourceSend(context, resourceName, options); - return _headResourceDeserialize(result); -} -``` - -# Non wrap HEAD void-only response as boolean (no body wrap) - -When `head-as-boolean` is true but `wrap-non-model-return` is false, the HEAD operation -returns a plain `boolean` (not wrapped in `{ body: boolean }`). - -## TypeSpec - -```tsp -@route("/resources/{resourceName}") -@head -op headResource(@path resourceName: string): void; -``` - -```yaml -wrap-non-model-return: false -head-as-boolean: true -``` - -## Operations - -```ts operations -import { TestingContext as Client } from "./index.js"; -import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; -import { HeadResourceOptionalParams } from "./options.js"; -import { - StreamableMethod, - PathUncheckedResponse, - createRestError, - operationOptionsToRequestParameters, -} from "@azure-rest/core-client"; - -export function _headResourceSend( - context: Client, - resourceName: string, - options: HeadResourceOptionalParams = { requestOptions: {} }, -): StreamableMethod { - const path = expandUrlTemplate( - "/resources/{resourceName}", - { - resourceName: resourceName, - }, - { - allowReserved: options?.requestOptions?.skipUrlEncoding, - }, - ); - return context.path(path).head({ ...operationOptionsToRequestParameters(options) }); -} - -export async function _headResourceDeserialize(result: PathUncheckedResponse): Promise { - const expectedStatuses = ["204", "404"]; - if (!expectedStatuses.includes(result.status)) { - throw createRestError(result); - } - - return result.status.startsWith("2"); -} - -export async function headResource( - context: Client, - resourceName: string, - options: HeadResourceOptionalParams = { requestOptions: {} }, -): Promise { - const result = await _headResourceSend(context, resourceName, options); - return _headResourceDeserialize(result); -} -``` - # HEAD void-only response as void body property ## TypeSpec @@ -1184,7 +960,6 @@ op headResource(@path resourceName: string): void; ```yaml wrap-non-model-return: false -head-as-boolean: false ``` ## Operations @@ -1248,7 +1023,6 @@ op headResource(@path resourceName: string): void; ```yaml wrap-non-model-return: true -head-as-boolean: false ``` ## Operations diff --git a/packages/typespec-ts/test/util/emitUtil.ts b/packages/typespec-ts/test/util/emitUtil.ts index aa954f5b83..dde8238a5b 100644 --- a/packages/typespec-ts/test/util/emitUtil.ts +++ b/packages/typespec-ts/test/util/emitUtil.ts @@ -431,9 +431,6 @@ export async function emitModularModelsFromTypeSpec( dpgContext.rlcOptions!.wrapNonModelReturn = options["wrap-non-model-return"] === true; } - if (options["head-as-boolean"] !== undefined) { - dpgContext.rlcOptions!.headAsBoolean = options["head-as-boolean"] === true; - } if (options["treat-unknown-as-record"] !== undefined) { dpgContext.rlcOptions!.treatUnknownAsRecord = options["treat-unknown-as-record"] === true; @@ -593,9 +590,6 @@ export async function emitModularOperationsFromTypeSpec( dpgContext.rlcOptions!.wrapNonModelReturn = options["wrap-non-model-return"] === true; } - if (options["head-as-boolean"] !== undefined) { - dpgContext.rlcOptions!.headAsBoolean = options["head-as-boolean"] === true; - } dpgContext.rlcOptions!.enableStorageCompat = options["enable-storage-compat"] === true; if (options["treat-unknown-as-record"] !== undefined) {