diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/package.json b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/package.json index 4447688190..07caff2d01 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/package.json +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/package.json @@ -12,39 +12,21 @@ "./package.json": "./package.json", ".": "./src/index.ts", "./api": "./src/api/index.ts", - "./api/budgets": "./src/api/budgets/index.ts", - "./api/sapWidgets": "./src/api/sapWidgets/index.ts", + "./api/budgets": "src/api/budgets/index.ts", + "./api/sapWidgets": "src/api/sapWidgets/index.ts", "./models": "./src/models/index.ts" }, - "dialects": [ - "esm", - "commonjs" - ], - "esmDialects": [ - "browser", - "react-native" - ], + "dialects": ["esm", "commonjs"], + "esmDialects": ["browser", "react-native"], "selfLink": false }, "type": "module", "browser": "./dist/browser/index.js", "react-native": "./dist/react-native/index.js", - "keywords": [ - "node", - "azure", - "cloud", - "typescript", - "browser", - "isomorphic" - ], + "keywords": ["node", "azure", "cloud", "typescript", "browser", "isomorphic"], "author": "Microsoft Corporation", "license": "MIT", - "files": [ - "dist/", - "!dist/**/*.d.*ts.map", - "README.md", - "LICENSE" - ], + "files": ["dist/", "!dist/**/*.d.*ts.map", "README.md", "LICENSE"], "dependencies": { "@azure/core-util": "^1.9.2", "@azure-rest/core-client": "^2.3.1", @@ -72,101 +54,5 @@ "lint": "eslint package.json api-extractor.json src", "lint:fix": "eslint package.json api-extractor.json src --fix --fix-type [problem,suggestion]", "build": "npm run clean && tshy && npm run extract-api" - }, - "exports": { - "./package.json": "./package.json", - ".": { - "browser": { - "types": "./dist/browser/index.d.ts", - "default": "./dist/browser/index.js" - }, - "react-native": { - "types": "./dist/react-native/index.d.ts", - "default": "./dist/react-native/index.js" - }, - "import": { - "types": "./dist/esm/index.d.ts", - "default": "./dist/esm/index.js" - }, - "require": { - "types": "./dist/commonjs/index.d.ts", - "default": "./dist/commonjs/index.js" - } - }, - "./api": { - "browser": { - "types": "./dist/browser/api/index.d.ts", - "default": "./dist/browser/api/index.js" - }, - "react-native": { - "types": "./dist/react-native/api/index.d.ts", - "default": "./dist/react-native/api/index.js" - }, - "import": { - "types": "./dist/esm/api/index.d.ts", - "default": "./dist/esm/api/index.js" - }, - "require": { - "types": "./dist/commonjs/api/index.d.ts", - "default": "./dist/commonjs/api/index.js" - } - }, - "./api/budgets": { - "browser": { - "types": "./dist/browser/api/budgets/index.d.ts", - "default": "./dist/browser/api/budgets/index.js" - }, - "react-native": { - "types": "./dist/react-native/api/budgets/index.d.ts", - "default": "./dist/react-native/api/budgets/index.js" - }, - "import": { - "types": "./dist/esm/api/budgets/index.d.ts", - "default": "./dist/esm/api/budgets/index.js" - }, - "require": { - "types": "./dist/commonjs/api/budgets/index.d.ts", - "default": "./dist/commonjs/api/budgets/index.js" - } - }, - "./api/sapWidgets": { - "browser": { - "types": "./dist/browser/api/sapWidgets/index.d.ts", - "default": "./dist/browser/api/sapWidgets/index.js" - }, - "react-native": { - "types": "./dist/react-native/api/sapWidgets/index.d.ts", - "default": "./dist/react-native/api/sapWidgets/index.js" - }, - "import": { - "types": "./dist/esm/api/sapWidgets/index.d.ts", - "default": "./dist/esm/api/sapWidgets/index.js" - }, - "require": { - "types": "./dist/commonjs/api/sapWidgets/index.d.ts", - "default": "./dist/commonjs/api/sapWidgets/index.js" - } - }, - "./models": { - "browser": { - "types": "./dist/browser/models/index.d.ts", - "default": "./dist/browser/models/index.js" - }, - "react-native": { - "types": "./dist/react-native/models/index.d.ts", - "default": "./dist/react-native/models/index.js" - }, - "import": { - "types": "./dist/esm/models/index.d.ts", - "default": "./dist/esm/models/index.js" - }, - "require": { - "types": "./dist/commonjs/models/index.d.ts", - "default": "./dist/commonjs/models/index.js" - } - } - }, - "main": "./dist/commonjs/index.js", - "types": "./dist/commonjs/index.d.ts", - "module": "./dist/esm/index.js" + } } diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/review/widget_dpg.api.md b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/review/widget_dpg.api.md deleted file mode 100644 index 6a15531a5a..0000000000 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/review/widget_dpg.api.md +++ /dev/null @@ -1,178 +0,0 @@ -## API Report File for "@msinternal/widget_dpg" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { AbortSignalLike } from '@azure/abort-controller'; -import { ClientOptions } from '@azure-rest/core-client'; -import { KeyCredential } from '@azure/core-auth'; -import { OperationOptions } from '@azure-rest/core-client'; -import { OperationState } from '@azure/core-lro'; -import { PathUncheckedResponse } from '@azure-rest/core-client'; -import { Pipeline } from '@azure/core-rest-pipeline'; -import { PollerLike } from '@azure/core-lro'; - -// @public -export interface AnalyzeResult { - // (undocumented) - summary: string; -} - -// @public -export interface BudgetsContinueOptionalParams extends OperationOptions { -} - -// @public -export interface BudgetsCreateOrReplaceOptionalParams extends OperationOptions { - updateIntervalInMs?: number; -} - -// @public -export interface BudgetsGetBudgetsOptionalParams extends OperationOptions { -} - -// @public -export interface BudgetsOperations { - continue: (options?: BudgetsContinueOptionalParams) => Promise; - createOrReplace: (name: string, resource: SAPUser, options?: BudgetsCreateOrReplaceOptionalParams) => PollerLike, SAPUser>; - // (undocumented) - getBudgets: (name: string, options?: BudgetsGetBudgetsOptionalParams) => Promise; -} - -// @public -export type ContinuablePage = TPage & { - continuationToken?: string; -}; - -// @public -export enum KnownVersions { - _100 = "1.0.0" -} - -// @public -export interface NonReferencedModel { - prop1: number; - prop2: string; -} - -// @public -export interface PagedAsyncIterableIterator { - [Symbol.asyncIterator](): PagedAsyncIterableIterator; - byPage: (settings?: TPageSettings) => AsyncIterableIterator>; - next(): Promise>; -} - -// @public -export interface PageSettings { - continuationToken?: string; -} - -// @public -export function restorePoller(client: SAPWidgetServiceClient, serializedState: string, sourceOperation: (...args: any[]) => PollerLike, TResult>, options?: RestorePollerOptions): PollerLike, TResult>; - -// @public (undocumented) -export interface RestorePollerOptions extends OperationOptions { - abortSignal?: AbortSignalLike; - processResponseBody?: (result: TResponse) => Promise; - updateIntervalInMs?: number; -} - -// @public -export interface SAPUser { - id: string; - readonly name: string; - role: string; -} - -// @public -export interface SAPWidgetsAnalyzeWidgetOptionalParams extends OperationOptions { -} - -// @public -export interface SAPWidgetsCreateOrReplaceOptionalParams extends OperationOptions { - updateIntervalInMs?: number; -} - -// @public -export interface SAPWidgetsCreateWidgetOptionalParams extends OperationOptions { -} - -// @public -export interface SAPWidgetsDeleteWidgetOptionalParams extends OperationOptions { -} - -// @public (undocumented) -export class SAPWidgetServiceClient { - constructor(endpointParam: string, credential: KeyCredential, options?: SAPWidgetServiceClientOptionalParams); - readonly budgets: BudgetsOperations; - readonly pipeline: Pipeline; - readonly sapWidgets: SAPWidgetsOperations; -} - -// @public -export interface SAPWidgetServiceClientOptionalParams extends ClientOptions { - apiVersion?: string; -} - -// @public -export interface SAPWidgetsGetWidgetOptionalParams extends OperationOptions { -} - -// @public -export interface SAPWidgetsListWidgetsPagesOptionalParams extends OperationOptions { -} - -// @public -export interface SAPWidgetsOperations { - analyzeWidget: (id: string, options?: SAPWidgetsAnalyzeWidgetOptionalParams) => Promise; - createOrReplace: (name: string, resource: SAPUser, options?: SAPWidgetsCreateOrReplaceOptionalParams) => PollerLike, SAPUser>; - createWidget: (weight: number, color: "red" | "blue", options?: SAPWidgetsCreateWidgetOptionalParams) => Promise; - deleteWidget: (id: string, options?: SAPWidgetsDeleteWidgetOptionalParams) => Promise; - getWidget: (id: string, options?: SAPWidgetsGetWidgetOptionalParams) => Promise; - // (undocumented) - listWidgetsPages: (page: number, pageSize: number, options?: SAPWidgetsListWidgetsPagesOptionalParams) => PagedAsyncIterableIterator; - // (undocumented) - queryWidgetsPages: (page: number, pageSize: number, options?: SAPWidgetsQueryWidgetsPagesOptionalParams) => PagedAsyncIterableIterator; - sapListWidgets: (requiredHeader: string, bytesHeader: Uint8Array, value: Uint8Array, csvArrayHeader: Uint8Array[], utcDateHeader: Date, options?: SAPWidgetsSAPListWidgetsOptionalParams) => Promise; - updateWidget: (id: string, options?: SAPWidgetsUpdateWidgetOptionalParams) => Promise; -} - -// @public -export interface SAPWidgetsQueryWidgetsPagesOptionalParams extends OperationOptions { -} - -// @public -export interface SAPWidgetsSAPListWidgetsOptionalParams extends OperationOptions { - // (undocumented) - nullableDateHeader?: Date | null; - // (undocumented) - nullableOptionalHeader?: string | null; - // (undocumented) - optionalDateHeader?: Date; - // (undocumented) - optionalHeader?: string; -} - -// @public -export interface SAPWidgetsUpdateWidgetOptionalParams extends OperationOptions { - color?: "red" | "blue"; - weight?: number; -} - -// @public -export interface Widget { - color: "red" | "blue"; - id: string; - weight: number; -} - -// @public -export interface WidgetError { - code: number; - message: string; -} - -// (No @packageDocumentation comment for this package) - -``` diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/package.json b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/package.json new file mode 100644 index 0000000000..5bbefffbab --- /dev/null +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts index 20eb75f78e..178adbf0e7 100644 --- a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts @@ -229,14 +229,24 @@ export function getDeserializePrivateFunction( } else if (isAzureCoreErrorType(context.program, deserializedType.__raw)) { statements.push(`return ${deserializedRoot}`); } else { + // Determine if the response contains binary payload + // Use __raw if it exists and is a Type, otherwise fall back to the type itself + const rawType = (response.type as any)?.__raw; + const responseType = + rawType && typeof rawType === "object" && "kind" in rawType + ? rawType + : response.type; + const isBinary = + responseType && contentTypes + ? isBinaryPayload(context, responseType as any, contentTypes) + : false; + statements.push( `return ${deserializeResponseValue( context, deserializedType, deserializedRoot, - isBinaryPayload(context, response.type!.__raw!, contentTypes!) - ? "binary" - : getEncodeForType(deserializedType) + isBinary ? "binary" : getEncodeForType(deserializedType) )}` ); } diff --git a/packages/typespec-ts/src/utils/modelUtils.ts b/packages/typespec-ts/src/utils/modelUtils.ts index 3f0773e819..d14e30cf13 100644 --- a/packages/typespec-ts/src/utils/modelUtils.ts +++ b/packages/typespec-ts/src/utils/modelUtils.ts @@ -96,19 +96,23 @@ export function getBinaryType(usage: SchemaContext[]) { } export function isByteOrByteUnion(dpgContext: SdkContext, type: Type) { + if (!type) { + return false; + } const schema = getSchemaForType(dpgContext, type); return isBytesType(schema) || isBytesUnion(schema); } function isBytesType(schema: any) { return ( + schema && schema.type === "string" && (schema.format === "bytes" || schema.format === "binary") ); } function isBytesUnion(schema: any) { - if (!Array.isArray(schema.enum)) { + if (!schema || !Array.isArray(schema.enum)) { return false; } for (const ele of schema.enum) { @@ -286,7 +290,7 @@ export function getEffectiveModelFromType( * If type is an anonymous model, tries to find a named model that has the same * set of properties when non-schema properties are excluded. */ - if (type.kind === "Model" && type.name === "") { + if (type && type.kind === "Model" && type.name === "") { const effective = getEffectiveModelType(context.program, type, (property) => isSchemaProperty(context.program, property) ); diff --git a/packages/typespec-ts/test/modularUnit/operations.spec.ts b/packages/typespec-ts/test/modularUnit/operations.spec.ts index 14899f8fdf..24efb559b7 100644 --- a/packages/typespec-ts/test/modularUnit/operations.spec.ts +++ b/packages/typespec-ts/test/modularUnit/operations.spec.ts @@ -47,4 +47,101 @@ describe("operations", () => { } }); }); + + describe("binary payload with application/cose", () => { + it("should handle binary response with application/cose content type without crashing", async () => { + const tspContent = ` + @doc("Signed statement") + model SignedStatement { + @doc("The MIME content type a Cose body is application/cose") + @header("Content-Type") + contentType: "application/cose"; + + @doc("CoseSign1 signature envelope") + @bodyRoot + body: bytes; + } + + @doc("Response with COSE binary content") + model CoseResponse { + @doc("Status code") + @statusCode + statusCode: 201; + + @doc("The MIME content type a Cose body is application/cose") + @header("Content-Type") + contentType: "application/cose"; + + @doc("Receipt body in COSE format") + @bodyRoot + body: bytes; + } + + @post + op createEntry(...SignedStatement): CoseResponse; + `; + + // This should not throw an error - previously would crash with "Cannot read properties of undefined (reading 'kind')" + const result = await emitModularOperationsFromTypeSpec(tspContent); + assert.ok(result); + // Verify that operations were generated successfully + assert.ok(result!.length > 0); + }); + }); + + describe("binary COSE response handling", () => { + it("should generate correct operations for binary COSE responses", async () => { + const tspContent = ` + @doc("Signed statement with binary COSE content") + model SignedStatement { + @doc("The MIME content type for COSE is application/cose") + @header("Content-Type") + contentType: "application/cose"; + + @doc("Binary COSE payload") + @bodyRoot + body: bytes; + } + + @doc("Response with binary COSE content") + model CoseResponse { + @doc("Status code") + @statusCode + statusCode: 201; + + @doc("The MIME content type for COSE is application/cose") + @header("Content-Type") + contentType: "application/cose"; + + @doc("Binary COSE response payload") + @bodyRoot + body: bytes; + } + + @post + @route("/submit") + op submitCoseData(...SignedStatement): CoseResponse; + + @get + @route("/cose") + op getCoseData(): CoseResponse; + `; + + const result = await emitModularOperationsFromTypeSpec(tspContent); + assert.ok(result); + assert.ok(result!.length > 0); + + // Check that binary response operations are generated correctly + const operationsFile = result!.find(file => file.getFilePath().endsWith("operations.ts")); + assert.ok(operationsFile, "operations.ts should be generated"); + + const operationsContent = operationsFile!.getFullText(); + + // Verify binary response handling + assert.ok(operationsContent.includes("Promise"), "Should return Uint8Array for binary responses"); + assert.ok(operationsContent.includes('"accept": "application/cose"'), "Should set correct accept header"); + assert.ok(operationsContent.includes('contentType: "application/cose"'), "Should set correct content type"); + assert.ok(operationsContent.includes("return result.body"), "Should return binary body directly"); + }); + }); }); diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/operations.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/operations.md index 5837861dc3..960a22968d 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/operations/operations.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/operations.md @@ -1187,3 +1187,133 @@ export async function createOrUpdateEndpoint( return _createOrUpdateEndpointDeserialize(result); } ``` + +# Binary COSE response handling + +Binary responses with application/cose content type should be properly handled without crashing. + +## TypeSpec + +```tsp +@doc("Signed statement with binary COSE content") +model SignedStatement { + @doc("The MIME content type for COSE is application/cose") + @header("Content-Type") + contentType: "application/cose"; + + @doc("Binary COSE payload") + @bodyRoot + body: bytes; +} + +@doc("Response with binary COSE content") +model CoseResponse { + @doc("Status code") + @statusCode + statusCode: 201; + + @doc("The MIME content type for COSE is application/cose") + @header("Content-Type") + contentType: "application/cose"; + + @doc("Binary COSE response payload") + @bodyRoot + body: bytes; +} + +@post +@route("/submit") +op submitCoseData(...SignedStatement): CoseResponse; + +@get +@route("/cose") +op getCoseData(): CoseResponse; +``` + +## Operations + +```ts operations +import { BinaryCoseTestContext as Client } from "./index.js"; +import { + GetCoseDataOptionalParams, + SubmitCoseDataOptionalParams, +} from "./options.js"; +import { + StreamableMethod, + PathUncheckedResponse, + createRestError, + operationOptionsToRequestParameters, +} from "@typespec/ts-http-runtime"; + +export function _getCoseDataSend( + context: Client, + options: GetCoseDataOptionalParams = { requestOptions: {} }, +): StreamableMethod { + return context + .path("/cose") + .get({ + ...operationOptionsToRequestParameters(options), + headers: { + accept: "application/cose", + ...options.requestOptions?.headers, + }, + }); +} + +export async function _getCoseDataDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["201"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return result.body; +} + +export async function getCoseData( + context: Client, + options: GetCoseDataOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _getCoseDataSend(context, options); + return _getCoseDataDeserialize(result); +} + +export function _submitCoseDataSend( + context: Client, + body: Uint8Array, + options: SubmitCoseDataOptionalParams = { requestOptions: {} }, +): StreamableMethod { + return context + .path("/submit") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: "application/cose", + headers: { + accept: "application/cose", + ...options.requestOptions?.headers, + }, + body: body, + }); +} + +export async function _submitCoseDataDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["201"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return result.body; +} + +export async function submitCoseData( + context: Client, + body: Uint8Array, + options: SubmitCoseDataOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _submitCoseDataSend(context, body, options); + return _submitCoseDataDeserialize(result); +} +```