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
15 changes: 0 additions & 15 deletions packages/typespec-ts/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -405,12 +396,6 @@ export const RLCOptionsSchema: JSONSchemaType<EmitterOptions> = {
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,
Expand Down
31 changes: 11 additions & 20 deletions packages/typespec-ts/src/modular/helpers/operationHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" };
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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" };
}

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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} }`;
Expand Down
2 changes: 0 additions & 2 deletions packages/typespec-ts/src/transform/transfromRLCOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -145,7 +144,6 @@ function extractRLCOptions(
isMultiService,
enableStorageCompat,
treatUnknownAsRecord,
headAsBoolean,
generateReactNativeTarget
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CheckExistenceResponse> {
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<CheckExistenceResponse> {
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<HeadResourceResponse> {
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<HeadResourceResponse> {
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<boolean> {
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<boolean> {
const result = await _headResourceSend(context, resourceName, options);
return _headResourceDeserialize(result);
}
```

# HEAD void-only response as void body property

## TypeSpec
Expand All @@ -1184,7 +960,6 @@ op headResource(@path resourceName: string): void;

```yaml
wrap-non-model-return: false
head-as-boolean: false
```

## Operations
Expand Down Expand Up @@ -1248,7 +1023,6 @@ op headResource(@path resourceName: string): void;

```yaml
wrap-non-model-return: true
head-as-boolean: false
```

## Operations
Expand Down
Loading
Loading