diff --git a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts index cef645c5ca..06e4b9bd2c 100644 --- a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts @@ -232,30 +232,42 @@ export function getDeserializePrivateFunction( if (deserializedType) { const contentTypes = operation.operation.responses[0]?.contentTypes; - const deserializeFunctionName = buildModelDeserializer( - context, - deserializedType, - { - nameOnly: true, - skipDiscriminatedUnionSuffix: false - } - ); - if (deserializeFunctionName) { - statements.push(`return ${deserializeFunctionName}(${deserializedRoot})`); - } else if (isAzureCoreErrorType(context.program, deserializedType.__raw)) { + // For File type responses, directly return the body + if ( + deserializedType.kind === "model" && + deserializedType.crossLanguageDefinitionId === "TypeSpec.Http.File" + ) { statements.push(`return ${deserializedRoot}`); } else { - statements.push( - `return ${deserializeResponseValue( - context, - deserializedType, - deserializedRoot, - true, - isBinaryPayload(context, response.type!.__raw!, contentTypes!) - ? "binary" - : getEncodeForType(deserializedType) - )}` + const deserializeFunctionName = buildModelDeserializer( + context, + deserializedType, + { + nameOnly: true, + skipDiscriminatedUnionSuffix: false + } ); + if (deserializeFunctionName) { + statements.push( + `return ${deserializeFunctionName}(${deserializedRoot})` + ); + } else if ( + isAzureCoreErrorType(context.program, deserializedType.__raw) + ) { + statements.push(`return ${deserializedRoot}`); + } else { + statements.push( + `return ${deserializeResponseValue( + context, + deserializedType, + deserializedRoot, + true, + isBinaryPayload(context, response.type!.__raw!, contentTypes!) + ? "binary" + : getEncodeForType(deserializedType) + )}` + ); + } } } else if (returnType.type === "void") { statements.push("return;"); @@ -501,7 +513,11 @@ export function getOperationFunction( const parameterList = parameters.map((p) => p.name).join(", "); // Special case for binary-only bodies: use helper to call streaming methods so that Core doesn't poison the response body by // doing a UTF-8 decode on the raw bytes. - if (response?.type?.kind === "bytes" && response.type.encode === "bytes") { + const isBinaryResponse = + (response?.type?.kind === "bytes" && response.type.encode === "bytes") || + (response?.type?.kind === "model" && + response.type.crossLanguageDefinitionId === "TypeSpec.Http.File"); + if (isBinaryResponse) { statements.push(`const streamableMethod = _${name}Send(${parameterList});`); statements.push( `const result = await ${resolveReference(SerializationHelpers.getBinaryResponse)}(streamableMethod);` @@ -1685,6 +1701,10 @@ export function deserializeResponseValue( return `${restValue} as any`; } case "model": // generate deserialize logic for spread model types + // Special handling for TypeSpec.Http.File - it's already a Uint8Array + if (type.crossLanguageDefinitionId === "TypeSpec.Http.File") { + return restValue; + } return `{${getResponseMapping(context, type, "").join(",")}}`; case "nullable": return deserializeResponseValue( diff --git a/packages/typespec-ts/src/modular/serialization/buildDeserializerFunction.ts b/packages/typespec-ts/src/modular/serialization/buildDeserializerFunction.ts index f78ccebd84..0bf6d94915 100644 --- a/packages/typespec-ts/src/modular/serialization/buildDeserializerFunction.ts +++ b/packages/typespec-ts/src/modular/serialization/buildDeserializerFunction.ts @@ -80,6 +80,13 @@ export function buildModelDeserializer( } ): FunctionDeclarationStructure | undefined | string { // const modelTcgcType = getTcgcType(type) as SdkModelType; + // TypeSpec.Http.File doesn't need deserialization - it's already a Uint8Array + if ( + type.kind === "model" && + type.crossLanguageDefinitionId === "TypeSpec.Http.File" + ) { + return undefined; + } if (!isSupportedSerializeType(type)) { return undefined; } diff --git a/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts b/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts index ca8c409611..6ce27ffa71 100644 --- a/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts +++ b/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts @@ -89,6 +89,10 @@ export function getTypeExpression( return `Record`; } case "model": + // Special handling for TypeSpec.Http.File + if (type.crossLanguageDefinitionId === "TypeSpec.Http.File") { + return "Uint8Array"; + } return getModelExpression(context, type); case "nullable": return getNullableExpression(context, type, options); diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md b/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md new file mode 100644 index 0000000000..2aa0253d62 --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/fileModel.md @@ -0,0 +1,78 @@ +# Should generate file model + +## TypeSpec + +```tsp +model Site { + name: string; +} +op test(@body site:Site): File<"application/octet-stream">; +``` + +## Model + +```ts models +/** + * This file contains only generated model types and their (de)serializers. + * Disable the following rules for internal models with '_' prefix and deserializers which require 'any' for raw JSON input. + */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/** model interface Site */ +export interface Site { + name: string; +} + +export function siteSerializer(item: Site): any { + return { name: item["name"] }; +} +``` + +## Operations + +```ts operations +import { TestingContext as Client } from "./index.js"; +import { Site, siteSerializer } from "../models/models.js"; +import { getBinaryResponse } from "../static-helpers/serialization/get-binary-response.js"; +import { TestOptionalParams } from "./options.js"; +import { + StreamableMethod, + PathUncheckedResponse, + createRestError, + operationOptionsToRequestParameters, +} from "@azure-rest/core-client"; + +export function _testSend( + context: Client, + site: Site, + options: TestOptionalParams = { requestOptions: {} }, +): StreamableMethod { + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: "application/json", + headers: { accept: "application/octet-stream", ...options.requestOptions?.headers }, + body: siteSerializer(site), + }); +} + +export async function _testDeserialize(result: PathUncheckedResponse): Promise { + const expectedStatuses = ["200"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return result.body; +} + +export async function test( + context: Client, + site: Site, + options: TestOptionalParams = { requestOptions: {} }, +): Promise { + const streamableMethod = _testSend(context, site, options); + const result = await getBinaryResponse(streamableMethod); + return _testDeserialize(result); +} +``` \ No newline at end of file