diff --git a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataProducts/index.ts b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataProducts/index.ts index 75f45998bd..0f2cc89fce 100644 --- a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataProducts/index.ts +++ b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataProducts/index.ts @@ -207,7 +207,7 @@ function _getDataProducts(context: NetworkAnalyticsApiContext) { }; } -export function getDataProductsOperations( +export function _getDataProductsOperations( context: NetworkAnalyticsApiContext, ): DataProductsOperations { return { diff --git a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataProductsCatalogs/index.ts b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataProductsCatalogs/index.ts index 91305fe56a..3d3083c422 100644 --- a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataProductsCatalogs/index.ts +++ b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataProductsCatalogs/index.ts @@ -49,7 +49,7 @@ function _getDataProductsCatalogs(context: NetworkAnalyticsApiContext) { }; } -export function getDataProductsCatalogsOperations( +export function _getDataProductsCatalogsOperations( context: NetworkAnalyticsApiContext, ): DataProductsCatalogsOperations { return { diff --git a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataTypes/index.ts b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataTypes/index.ts index ad12e6b55e..3d034a41e3 100644 --- a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataTypes/index.ts +++ b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/dataTypes/index.ts @@ -181,7 +181,7 @@ function _getDataTypes(context: NetworkAnalyticsApiContext) { }; } -export function getDataTypesOperations( +export function _getDataTypesOperations( context: NetworkAnalyticsApiContext, ): DataTypesOperations { return { diff --git a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/operations/index.ts b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/operations/index.ts index ae54998faa..7da64a18ed 100644 --- a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/operations/index.ts +++ b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/classic/operations/index.ts @@ -21,7 +21,7 @@ function _getOperations(context: NetworkAnalyticsApiContext) { }; } -export function getOperationsOperations( +export function _getOperationsOperations( context: NetworkAnalyticsApiContext, ): OperationsOperations { return { diff --git a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/networkAnalyticsApi.ts b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/networkAnalyticsApi.ts index 94e8de4c3f..37c0be6577 100644 --- a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/networkAnalyticsApi.ts +++ b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/networkAnalyticsApi.ts @@ -2,19 +2,19 @@ // Licensed under the MIT License. import { - getDataProductsOperations, + _getDataProductsOperations, DataProductsOperations, } from "./classic/dataProducts/index.js"; import { - getDataTypesOperations, + _getDataTypesOperations, DataTypesOperations, } from "./classic/dataTypes/index.js"; import { - getDataProductsCatalogsOperations, + _getDataProductsCatalogsOperations, DataProductsCatalogsOperations, } from "./classic/dataProductsCatalogs/index.js"; import { - getOperationsOperations, + _getOperationsOperations, OperationsOperations, } from "./classic/operations/index.js"; import { @@ -46,10 +46,12 @@ export class NetworkAnalyticsApi { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.dataProducts = getDataProductsOperations(this._client); - this.dataTypes = getDataTypesOperations(this._client); - this.dataProductsCatalogs = getDataProductsCatalogsOperations(this._client); - this.operations = getOperationsOperations(this._client); + this.dataProducts = _getDataProductsOperations(this._client); + this.dataTypes = _getDataTypesOperations(this._client); + this.dataProductsCatalogs = _getDataProductsCatalogsOperations( + this._client, + ); + this.operations = _getOperationsOperations(this._client); } /** The operation groups for dataProducts */ diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/review/ai-client.api.md b/packages/typespec-test/test/ai/generated/typespec-ts/review/ai-client.api.md index 54365dac32..a9a6d31970 100644 --- a/packages/typespec-test/test/ai/generated/typespec-ts/review/ai-client.api.md +++ b/packages/typespec-test/test/ai/generated/typespec-ts/review/ai-client.api.md @@ -646,6 +646,9 @@ export interface FileContentResponse { content: Uint8Array; } +// @public +export type FileContents = string | NodeJS.ReadableStream | ReadableStream | Uint8Array | Blob; + // @public export interface FileDeletionStatus { deleted: boolean; diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/azureAIClient.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/azureAIClient.ts index 420bb83754..719d555feb 100644 --- a/packages/typespec-test/test/ai/generated/typespec-ts/src/azureAIClient.ts +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/azureAIClient.ts @@ -2,15 +2,15 @@ // Licensed under the MIT License. import { - getEvaluationsOperations, + _getEvaluationsOperations, EvaluationsOperations, } from "./classic/evaluations/index.js"; import { - getConnectionsOperations, + _getConnectionsOperations, ConnectionsOperations, } from "./classic/connections/index.js"; import { - getAgentsOperations, + _getAgentsOperations, AgentsOperations, } from "./classic/agents/index.js"; import { @@ -49,9 +49,9 @@ export class AzureAIClient { { ...options, userAgentOptions: { userAgentPrefix } }, ); this.pipeline = this._client.pipeline; - this.evaluations = getEvaluationsOperations(this._client); - this.connections = getConnectionsOperations(this._client); - this.agents = getAgentsOperations(this._client); + this.evaluations = _getEvaluationsOperations(this._client); + this.connections = _getConnectionsOperations(this._client); + this.agents = _getAgentsOperations(this._client); } /** The operation groups for evaluations */ diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/agents/index.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/agents/index.ts index 3fda7d8c37..779368ccb8 100644 --- a/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/agents/index.ts +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/agents/index.ts @@ -502,7 +502,9 @@ function _getAgents(context: AzureAIContext) { }; } -export function getAgentsOperations(context: AzureAIContext): AgentsOperations { +export function _getAgentsOperations( + context: AzureAIContext, +): AgentsOperations { return { ..._getAgents(context), }; diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/connections/index.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/connections/index.ts index 7198e56eca..267bb1fa05 100644 --- a/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/connections/index.ts +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/connections/index.ts @@ -45,7 +45,7 @@ function _getConnections(context: AzureAIContext) { }; } -export function getConnectionsOperations( +export function _getConnectionsOperations( context: AzureAIContext, ): ConnectionsOperations { return { diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/evaluations/index.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/evaluations/index.ts index 62236dfb93..3aa3d2fd5c 100644 --- a/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/evaluations/index.ts +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/classic/evaluations/index.ts @@ -99,7 +99,7 @@ function _getEvaluations(context: AzureAIContext) { }; } -export function getEvaluationsOperations( +export function _getEvaluationsOperations( context: AzureAIContext, ): EvaluationsOperations { return { diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/index.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/index.ts index ed4a1a6220..469d64771c 100644 --- a/packages/typespec-test/test/ai/generated/typespec-ts/src/index.ts +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/index.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { FileContents } from "./static-helpers/multipartHelpers.js"; import { PageSettings, ContinuablePage, @@ -276,3 +277,4 @@ export { EvaluationsOperations, } from "./classic/index.js"; export { PageSettings, ContinuablePage, PagedAsyncIterableIterator }; +export { FileContents }; diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/static-helpers/multipartHelpers.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/static-helpers/multipartHelpers.ts new file mode 100644 index 0000000000..a2770ad2ab --- /dev/null +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/static-helpers/multipartHelpers.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Valid values for the contents of a binary file. + */ +export type FileContents = + | string + | NodeJS.ReadableStream + | ReadableStream + | Uint8Array + | Blob; + +export function createFilePartDescriptor( + partName: string, + fileInput: any, + defaultContentType?: string, +): any { + if (fileInput.contents) { + return { + name: partName, + body: fileInput.contents, + contentType: fileInput.contentType ?? defaultContentType, + filename: fileInput.filename, + }; + } else { + return { + name: partName, + body: fileInput, + contentType: defaultContentType, + }; + } +} diff --git a/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/anomalyDetectorClient.ts b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/anomalyDetectorClient.ts index ae8edb2927..602290acd3 100644 --- a/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/anomalyDetectorClient.ts +++ b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/anomalyDetectorClient.ts @@ -2,11 +2,11 @@ // Licensed under the MIT License. import { - getMultivariateOperations, + _getMultivariateOperations, MultivariateOperations, } from "./classic/multivariate/index.js"; import { - getUnivariateOperations, + _getUnivariateOperations, UnivariateOperations, } from "./classic/univariate/index.js"; import { @@ -56,8 +56,8 @@ export class AnomalyDetectorClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.multivariate = getMultivariateOperations(this._client); - this.univariate = getUnivariateOperations(this._client); + this.multivariate = _getMultivariateOperations(this._client); + this.univariate = _getUnivariateOperations(this._client); } /** The operation groups for multivariate */ diff --git a/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/classic/multivariate/index.ts b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/classic/multivariate/index.ts index 7ea35f15ba..20f8506bd7 100644 --- a/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/classic/multivariate/index.ts +++ b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/classic/multivariate/index.ts @@ -132,7 +132,7 @@ function _getMultivariate(context: AnomalyDetectorContext) { }; } -export function getMultivariateOperations( +export function _getMultivariateOperations( context: AnomalyDetectorContext, ): MultivariateOperations { return { diff --git a/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/classic/univariate/index.ts b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/classic/univariate/index.ts index e449b9c7da..c99baab561 100644 --- a/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/classic/univariate/index.ts +++ b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/classic/univariate/index.ts @@ -64,7 +64,7 @@ function _getUnivariate(context: AnomalyDetectorContext) { }; } -export function getUnivariateOperations( +export function _getUnivariateOperations( context: AnomalyDetectorContext, ): UnivariateOperations { return { diff --git a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/c/index.ts b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/c/index.ts index 221b07900a..fff2c8a477 100644 --- a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/c/index.ts +++ b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/c/index.ts @@ -18,7 +18,7 @@ function _getBC(context: FooContext) { }; } -export function getBCOperations(context: FooContext): BCOperations { +export function _getBCOperations(context: FooContext): BCOperations { return { ..._getBC(context), }; diff --git a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/e/c/index.ts b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/e/c/index.ts index 8d876a1984..94b4d465cc 100644 --- a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/e/c/index.ts +++ b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/e/c/index.ts @@ -18,7 +18,7 @@ function _getBEC(context: FooContext) { }; } -export function getBECOperations(context: FooContext): BECOperations { +export function _getBECOperations(context: FooContext): BECOperations { return { ..._getBEC(context), }; diff --git a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/e/index.ts b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/e/index.ts index f621aa5325..e5ef04f2f9 100644 --- a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/e/index.ts +++ b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/e/index.ts @@ -2,15 +2,15 @@ // Licensed under the MIT License. import { FooContext } from "../../../api/fooContext.js"; -import { BECOperations, getBECOperations } from "./c/index.js"; +import { BECOperations, _getBECOperations } from "./c/index.js"; /** Interface representing a BE operations. */ export interface BEOperations { c: BECOperations; } -export function getBEOperations(context: FooContext): BEOperations { +export function _getBEOperations(context: FooContext): BEOperations { return { - c: getBECOperations(context), + c: _getBECOperations(context), }; } diff --git a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/index.ts b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/index.ts index e32df0dd58..93b2680e3a 100644 --- a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/index.ts +++ b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/b/index.ts @@ -5,8 +5,8 @@ import { FooContext } from "../../api/fooContext.js"; import { op1 } from "../../api/b/index.js"; import { Ba } from "../../models/b/models.js"; import { BOp1OptionalParams } from "../../api/options.js"; -import { BCOperations, getBCOperations } from "./c/index.js"; -import { BEOperations, getBEOperations } from "./e/index.js"; +import { BCOperations, _getBCOperations } from "./c/index.js"; +import { BEOperations, _getBEOperations } from "./e/index.js"; /** Interface representing a B operations. */ export interface BOperations { @@ -22,10 +22,10 @@ function _getB(context: FooContext) { }; } -export function getBOperations(context: FooContext): BOperations { +export function _getBOperations(context: FooContext): BOperations { return { ..._getB(context), - c: getBCOperations(context), - e: getBEOperations(context), + c: _getBCOperations(context), + e: _getBEOperations(context), }; } diff --git a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/d/index.ts b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/d/index.ts index 3c079b901f..55b99dafc4 100644 --- a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/d/index.ts +++ b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/classic/d/index.ts @@ -17,7 +17,7 @@ function _getD(context: FooContext) { }; } -export function getDOperations(context: FooContext): DOperations { +export function _getDOperations(context: FooContext): DOperations { return { ..._getD(context), }; diff --git a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/fooClient.ts b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/fooClient.ts index c60543c5d9..ee6fcd059a 100644 --- a/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/fooClient.ts +++ b/packages/typespec-test/test/hierarchy_generic/generated/typespec-ts/src/fooClient.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { getDOperations, DOperations } from "./classic/d/index.js"; -import { getBOperations, BOperations } from "./classic/b/index.js"; +import { _getDOperations, DOperations } from "./classic/d/index.js"; +import { _getBOperations, BOperations } from "./classic/b/index.js"; import { createFoo, FooContext, @@ -30,8 +30,8 @@ export class FooClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.d = getDOperations(this._client); - this.b = getBOperations(this._client); + this.d = _getDOperations(this._client); + this.b = _getBOperations(this._client); } /** The operation groups for d */ diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/review/openai-generic.api.md b/packages/typespec-test/test/openai_generic/generated/typespec-ts/review/openai-generic.api.md index 58ee8c8f19..131a23991d 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/review/openai-generic.api.md +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/review/openai-generic.api.md @@ -225,7 +225,11 @@ export interface CreateEmbeddingResponse { // @public export interface CreateFileRequest { - file: Uint8Array; + file: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; purpose: string; } @@ -258,8 +262,16 @@ export interface CreateFineTuningJobRequest { // @public export interface CreateImageEditRequest { - image: Uint8Array; - mask?: Uint8Array; + image: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; + mask?: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; n?: number | null; prompt: string; response_format?: ("url" | "b64_json") | null; @@ -280,7 +292,11 @@ export interface CreateImageRequest { // @public export interface CreateImageVariationRequest { - image: Uint8Array; + image: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; n?: number | null; response_format?: ("url" | "b64_json") | null; size?: ("256x256" | "512x512" | "1024x1024") | null; @@ -331,7 +347,11 @@ export interface CreateModerationResponse { // @public export interface CreateTranscriptionRequest { - file: Uint8Array; + file: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; language?: string; model: "whisper-1"; prompt?: string; @@ -347,7 +367,11 @@ export interface CreateTranscriptionResponse { // @public export interface CreateTranslationRequest { - file: Uint8Array; + file: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; model: "whisper-1"; prompt?: string; response_format?: "json" | "text" | "srt" | "verbose_json" | "vtt"; @@ -425,6 +449,9 @@ export interface ErrorResponse { error: ErrorModel; } +// @public +export type FileContents = string | NodeJS.ReadableStream | ReadableStream | Uint8Array | Blob; + // @public export interface FilesCreateOptionalParams extends OperationOptions { } diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/index.ts index d11286f91e..04c77d3944 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/index.ts @@ -4,11 +4,11 @@ import { OpenAIContext } from "../../api/openAIContext.js"; import { AudioTranscriptionsOperations, - getAudioTranscriptionsOperations, + _getAudioTranscriptionsOperations, } from "./transcriptions/index.js"; import { AudioTranslationsOperations, - getAudioTranslationsOperations, + _getAudioTranslationsOperations, } from "./translations/index.js"; /** Interface representing a Audio operations. */ @@ -17,9 +17,9 @@ export interface AudioOperations { transcriptions: AudioTranscriptionsOperations; } -export function getAudioOperations(context: OpenAIContext): AudioOperations { +export function _getAudioOperations(context: OpenAIContext): AudioOperations { return { - translations: getAudioTranslationsOperations(context), - transcriptions: getAudioTranscriptionsOperations(context), + translations: _getAudioTranslationsOperations(context), + transcriptions: _getAudioTranscriptionsOperations(context), }; } diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/transcriptions/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/transcriptions/index.ts index b233a3b5c0..a04f893f2f 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/transcriptions/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/transcriptions/index.ts @@ -26,7 +26,7 @@ function _getAudioTranscriptions(context: OpenAIContext) { }; } -export function getAudioTranscriptionsOperations( +export function _getAudioTranscriptionsOperations( context: OpenAIContext, ): AudioTranscriptionsOperations { return { diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/translations/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/translations/index.ts index 21a2fd9cc4..b1f3c432b2 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/translations/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/audio/translations/index.ts @@ -26,7 +26,7 @@ function _getAudioTranslations(context: OpenAIContext) { }; } -export function getAudioTranslationsOperations( +export function _getAudioTranslationsOperations( context: OpenAIContext, ): AudioTranslationsOperations { return { diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/chat/completions/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/chat/completions/index.ts index e8b1761e01..feaaa8c8d6 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/chat/completions/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/chat/completions/index.ts @@ -26,7 +26,7 @@ function _getChatCompletions(context: OpenAIContext) { }; } -export function getChatCompletionsOperations( +export function _getChatCompletionsOperations( context: OpenAIContext, ): ChatCompletionsOperations { return { diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/chat/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/chat/index.ts index 6fa08609e1..07fecf9677 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/chat/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/chat/index.ts @@ -4,7 +4,7 @@ import { OpenAIContext } from "../../api/openAIContext.js"; import { ChatCompletionsOperations, - getChatCompletionsOperations, + _getChatCompletionsOperations, } from "./completions/index.js"; /** Interface representing a Chat operations. */ @@ -12,8 +12,8 @@ export interface ChatOperations { completions: ChatCompletionsOperations; } -export function getChatOperations(context: OpenAIContext): ChatOperations { +export function _getChatOperations(context: OpenAIContext): ChatOperations { return { - completions: getChatCompletionsOperations(context), + completions: _getChatCompletionsOperations(context), }; } diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/completions/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/completions/index.ts index 7620381a72..b0309fd4a2 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/completions/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/completions/index.ts @@ -26,7 +26,7 @@ function _getCompletions(context: OpenAIContext) { }; } -export function getCompletionsOperations( +export function _getCompletionsOperations( context: OpenAIContext, ): CompletionsOperations { return { diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/edits/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/edits/index.ts index e18b6d2ebf..673d730fbd 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/edits/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/edits/index.ts @@ -21,7 +21,7 @@ function _getEdits(context: OpenAIContext) { }; } -export function getEditsOperations(context: OpenAIContext): EditsOperations { +export function _getEditsOperations(context: OpenAIContext): EditsOperations { return { ..._getEdits(context), }; diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/embeddings/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/embeddings/index.ts index 8088245e26..849ebe49ac 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/embeddings/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/embeddings/index.ts @@ -26,7 +26,7 @@ function _getEmbeddings(context: OpenAIContext) { }; } -export function getEmbeddingsOperations( +export function _getEmbeddingsOperations( context: OpenAIContext, ): EmbeddingsOperations { return { diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/files/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/files/index.ts index 207c353cd5..2fb93a9e2c 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/files/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/files/index.ts @@ -63,7 +63,7 @@ function _getFiles(context: OpenAIContext) { }; } -export function getFilesOperations(context: OpenAIContext): FilesOperations { +export function _getFilesOperations(context: OpenAIContext): FilesOperations { return { ..._getFiles(context), }; diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTunes/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTunes/index.ts index 11626b98c6..ffc00b1d78 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTunes/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTunes/index.ts @@ -64,7 +64,7 @@ function _getFineTunes(context: OpenAIContext) { }; } -export function getFineTunesOperations( +export function _getFineTunesOperations( context: OpenAIContext, ): FineTunesOperations { return { diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTuning/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTuning/index.ts index 59fe849ce8..7636b1bcb2 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTuning/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTuning/index.ts @@ -4,7 +4,7 @@ import { OpenAIContext } from "../../api/openAIContext.js"; import { FineTuningJobsOperations, - getFineTuningJobsOperations, + _getFineTuningJobsOperations, } from "./jobs/index.js"; /** Interface representing a FineTuning operations. */ @@ -12,10 +12,10 @@ export interface FineTuningOperations { jobs: FineTuningJobsOperations; } -export function getFineTuningOperations( +export function _getFineTuningOperations( context: OpenAIContext, ): FineTuningOperations { return { - jobs: getFineTuningJobsOperations(context), + jobs: _getFineTuningJobsOperations(context), }; } diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTuning/jobs/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTuning/jobs/index.ts index c74c3c3328..ddb510ebd0 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTuning/jobs/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/fineTuning/jobs/index.ts @@ -77,7 +77,7 @@ function _getFineTuningJobs(context: OpenAIContext) { }; } -export function getFineTuningJobsOperations( +export function _getFineTuningJobsOperations( context: OpenAIContext, ): FineTuningJobsOperations { return { diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/images/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/images/index.ts index 805d02eb19..46d5599ffc 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/images/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/images/index.ts @@ -46,7 +46,7 @@ function _getImages(context: OpenAIContext) { }; } -export function getImagesOperations(context: OpenAIContext): ImagesOperations { +export function _getImagesOperations(context: OpenAIContext): ImagesOperations { return { ..._getImages(context), }; diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/models/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/models/index.ts index ce593924dc..a2333138a6 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/models/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/models/index.ts @@ -42,7 +42,7 @@ function _getModels(context: OpenAIContext) { }; } -export function getModelsOperations(context: OpenAIContext): ModelsOperations { +export function _getModelsOperations(context: OpenAIContext): ModelsOperations { return { ..._getModels(context), }; diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/moderations/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/moderations/index.ts index 9b79405bf3..716b52c5c0 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/moderations/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/classic/moderations/index.ts @@ -26,7 +26,7 @@ function _getModerations(context: OpenAIContext) { }; } -export function getModerationsOperations( +export function _getModerationsOperations( context: OpenAIContext, ): ModerationsOperations { return { diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/index.ts index 9062045ab1..6948700d4a 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/index.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { FileContents } from "./static-helpers/multipartHelpers.js"; + export { OpenAIClient } from "./openAIClient.js"; export { CreateModerationRequest, @@ -99,3 +101,4 @@ export { ChatCompletionsOperations, FineTuningJobsOperations, } from "./classic/index.js"; +export { FileContents }; diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/models.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/models.ts index a3489540ae..eb36935e2b 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/models.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/models.ts @@ -1,7 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { uint8ArrayToString, stringToUint8Array } from "@azure/core-util"; +import { + FileContents, + createFilePartDescriptor, +} from "../static-helpers/multipartHelpers.js"; +import { stringToUint8Array } from "@azure/core-util"; /** model interface CreateModerationRequest */ export interface CreateModerationRequest { @@ -344,13 +348,17 @@ export interface CreateImageEditRequest { * The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not * provided, image must have transparency, which will be used as the mask. */ - image: Uint8Array; + image: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** * An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where * `image` should be edited. Must be a valid PNG file, less than 4MB, and have the same dimensions * as `image`. */ - mask?: Uint8Array; + mask?: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** The number of images to generate. Must be between 1 and 10. */ n?: number | null; /** The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`. */ @@ -363,17 +371,23 @@ export interface CreateImageEditRequest { export function createImageEditRequestSerializer( item: CreateImageEditRequest, ): any { - return { - prompt: item["prompt"], - image: uint8ArrayToString(item["image"], "base64"), - mask: !item["mask"] - ? item["mask"] - : uint8ArrayToString(item["mask"], "base64"), - n: item["n"], - size: item["size"], - response_format: item["response_format"], - user: item["user"], - }; + return [ + { name: "prompt", body: item["prompt"] }, + createFilePartDescriptor("image", item["image"]), + ...(item["mask"] === undefined + ? [] + : [createFilePartDescriptor("mask", item["mask"])]), + ...(item["n"] === undefined ? [] : [{ name: "n", body: item["n"] }]), + ...(item["size"] === undefined + ? [] + : [{ name: "size", body: item["size"] }]), + ...(item["response_format"] === undefined + ? [] + : [{ name: "response_format", body: item["response_format"] }]), + ...(item["user"] === undefined + ? [] + : [{ name: "user", body: item["user"] }]), + ]; } /** model interface CreateImageVariationRequest */ @@ -382,7 +396,9 @@ export interface CreateImageVariationRequest { * The image to use as the basis for the variation(s). Must be a valid PNG file, less than 4MB, * and square. */ - image: Uint8Array; + image: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** The number of images to generate. Must be between 1 and 10. */ n?: number | null; /** The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`. */ @@ -395,13 +411,19 @@ export interface CreateImageVariationRequest { export function createImageVariationRequestSerializer( item: CreateImageVariationRequest, ): any { - return { - image: uint8ArrayToString(item["image"], "base64"), - n: item["n"], - size: item["size"], - response_format: item["response_format"], - user: item["user"], - }; + return [ + createFilePartDescriptor("image", item["image"]), + ...(item["n"] === undefined ? [] : [{ name: "n", body: item["n"] }]), + ...(item["size"] === undefined + ? [] + : [{ name: "size", body: item["size"] }]), + ...(item["response_format"] === undefined + ? [] + : [{ name: "response_format", body: item["response_format"] }]), + ...(item["user"] === undefined + ? [] + : [{ name: "user", body: item["user"] }]), + ]; } /** model interface ListModelsResponse */ @@ -829,7 +851,9 @@ export interface CreateFileRequest { * * If the `purpose` is set to "fine-tune", the file will be used for fine-tuning. */ - file: Uint8Array; + file: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** * The intended purpose of the uploaded documents. Use "fine-tune" for * [fine-tuning](/docs/api-reference/fine-tuning). This allows us to validate the format of the @@ -839,10 +863,10 @@ export interface CreateFileRequest { } export function createFileRequestSerializer(item: CreateFileRequest): any { - return { - file: uint8ArrayToString(item["file"], "base64"), - purpose: item["purpose"], - }; + return [ + createFilePartDescriptor("file", item["file"]), + { name: "purpose", body: item["purpose"] }, + ]; } /** model interface DeleteFileResponse */ @@ -2047,7 +2071,9 @@ export interface CreateTranslationRequest { * The audio file object (not file name) to translate, in one of these formats: flac, mp3, mp4, * mpeg, mpga, m4a, ogg, wav, or webm. */ - file: Uint8Array; + file: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** ID of the model to use. Only `whisper-1` is currently available. */ model: "whisper-1"; /** @@ -2072,13 +2098,19 @@ export interface CreateTranslationRequest { export function createTranslationRequestSerializer( item: CreateTranslationRequest, ): any { - return { - file: uint8ArrayToString(item["file"], "base64"), - model: item["model"], - prompt: item["prompt"], - response_format: item["response_format"], - temperature: item["temperature"], - }; + return [ + createFilePartDescriptor("file", item["file"]), + { name: "model", body: item["model"] }, + ...(item["prompt"] === undefined + ? [] + : [{ name: "prompt", body: item["prompt"] }]), + ...(item["response_format"] === undefined + ? [] + : [{ name: "response_format", body: item["response_format"] }]), + ...(item["temperature"] === undefined + ? [] + : [{ name: "temperature", body: item["temperature"] }]), + ]; } /** model interface CreateTranslationResponse */ @@ -2100,7 +2132,9 @@ export interface CreateTranscriptionRequest { * The audio file object (not file name) to transcribe, in one of these formats: flac, mp3, mp4, * mpeg, mpga, m4a, ogg, wav, or webm. */ - file: Uint8Array; + file: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** ID of the model to use. Only `whisper-1` is currently available. */ model: "whisper-1"; /** @@ -2131,14 +2165,22 @@ export interface CreateTranscriptionRequest { export function createTranscriptionRequestSerializer( item: CreateTranscriptionRequest, ): any { - return { - file: uint8ArrayToString(item["file"], "base64"), - model: item["model"], - prompt: item["prompt"], - response_format: item["response_format"], - temperature: item["temperature"], - language: item["language"], - }; + return [ + createFilePartDescriptor("file", item["file"]), + { name: "model", body: item["model"] }, + ...(item["prompt"] === undefined + ? [] + : [{ name: "prompt", body: item["prompt"] }]), + ...(item["response_format"] === undefined + ? [] + : [{ name: "response_format", body: item["response_format"] }]), + ...(item["temperature"] === undefined + ? [] + : [{ name: "temperature", body: item["temperature"] }]), + ...(item["language"] === undefined + ? [] + : [{ name: "language", body: item["language"] }]), + ]; } /** model interface CreateTranscriptionResponse */ diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/openAIClient.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/openAIClient.ts index 78a6912612..3118a7af47 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/openAIClient.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/openAIClient.ts @@ -2,37 +2,37 @@ // Licensed under the MIT License. import { - getModerationsOperations, + _getModerationsOperations, ModerationsOperations, } from "./classic/moderations/index.js"; import { - getImagesOperations, + _getImagesOperations, ImagesOperations, } from "./classic/images/index.js"; import { - getModelsOperations, + _getModelsOperations, ModelsOperations, } from "./classic/models/index.js"; import { - getFineTunesOperations, + _getFineTunesOperations, FineTunesOperations, } from "./classic/fineTunes/index.js"; -import { getFilesOperations, FilesOperations } from "./classic/files/index.js"; +import { _getFilesOperations, FilesOperations } from "./classic/files/index.js"; import { - getEmbeddingsOperations, + _getEmbeddingsOperations, EmbeddingsOperations, } from "./classic/embeddings/index.js"; -import { getEditsOperations, EditsOperations } from "./classic/edits/index.js"; +import { _getEditsOperations, EditsOperations } from "./classic/edits/index.js"; import { - getCompletionsOperations, + _getCompletionsOperations, CompletionsOperations, } from "./classic/completions/index.js"; import { - getFineTuningOperations, + _getFineTuningOperations, FineTuningOperations, } from "./classic/fineTuning/index.js"; -import { getChatOperations, ChatOperations } from "./classic/chat/index.js"; -import { getAudioOperations, AudioOperations } from "./classic/audio/index.js"; +import { _getChatOperations, ChatOperations } from "./classic/chat/index.js"; +import { _getAudioOperations, AudioOperations } from "./classic/audio/index.js"; import { createOpenAI, OpenAIContext, @@ -62,17 +62,17 @@ export class OpenAIClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.moderations = getModerationsOperations(this._client); - this.images = getImagesOperations(this._client); - this.models = getModelsOperations(this._client); - this.fineTunes = getFineTunesOperations(this._client); - this.files = getFilesOperations(this._client); - this.embeddings = getEmbeddingsOperations(this._client); - this.edits = getEditsOperations(this._client); - this.completions = getCompletionsOperations(this._client); - this.fineTuning = getFineTuningOperations(this._client); - this.chat = getChatOperations(this._client); - this.audio = getAudioOperations(this._client); + this.moderations = _getModerationsOperations(this._client); + this.images = _getImagesOperations(this._client); + this.models = _getModelsOperations(this._client); + this.fineTunes = _getFineTunesOperations(this._client); + this.files = _getFilesOperations(this._client); + this.embeddings = _getEmbeddingsOperations(this._client); + this.edits = _getEditsOperations(this._client); + this.completions = _getCompletionsOperations(this._client); + this.fineTuning = _getFineTuningOperations(this._client); + this.chat = _getChatOperations(this._client); + this.audio = _getAudioOperations(this._client); } /** The operation groups for moderations */ diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/static-helpers/multipartHelpers.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/static-helpers/multipartHelpers.ts new file mode 100644 index 0000000000..a2770ad2ab --- /dev/null +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/static-helpers/multipartHelpers.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Valid values for the contents of a binary file. + */ +export type FileContents = + | string + | NodeJS.ReadableStream + | ReadableStream + | Uint8Array + | Blob; + +export function createFilePartDescriptor( + partName: string, + fileInput: any, + defaultContentType?: string, +): any { + if (fileInput.contents) { + return { + name: partName, + body: fileInput.contents, + contentType: fileInput.contentType ?? defaultContentType, + filename: fileInput.filename, + }; + } else { + return { + name: partName, + body: fileInput, + contentType: defaultContentType, + }; + } +} diff --git a/packages/typespec-test/test/openai_modular/generated/typespec-ts/review/openai_modular.api.md b/packages/typespec-test/test/openai_modular/generated/typespec-ts/review/openai_modular.api.md index 4766f76ba8..ebd5ccb10c 100644 --- a/packages/typespec-test/test/openai_modular/generated/typespec-ts/review/openai_modular.api.md +++ b/packages/typespec-test/test/openai_modular/generated/typespec-ts/review/openai_modular.api.md @@ -29,7 +29,11 @@ export type AudioTranscriptionFormat = "json" | "verbose_json" | "text" | "srt" // @public export interface AudioTranscriptionOptions { - file: Uint8Array; + file: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; filename?: string; language?: string; model?: string; @@ -77,7 +81,11 @@ export type AudioTranslationFormat = "json" | "verbose_json" | "text" | "srt" | // @public export interface AudioTranslationOptions { - file: Uint8Array; + file: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; filename?: string; model?: string; prompt?: string; @@ -705,6 +713,9 @@ export interface EmbeddingsUsage { totalTokens: number; } +// @public +export type FileContents = string | NodeJS.ReadableStream | ReadableStream | Uint8Array | Blob; + // @public export interface FunctionCall { arguments: string; diff --git a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/index.ts b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/index.ts index 9d384be0c3..5886e9b40f 100644 --- a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/index.ts +++ b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/index.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { FileContents } from "./static-helpers/multipartHelpers.js"; + export { OpenAIClient } from "./openAIClient.js"; export { AudioTranscriptionOptions, @@ -157,3 +159,4 @@ export { GetAudioTranscriptionAsResponseObjectOptionalParams, GetAudioTranscriptionAsPlainTextOptionalParams, } from "./api/index.js"; +export { FileContents }; diff --git a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/models/models.ts b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/models/models.ts index 75c3fd4e82..1b1f325f3f 100644 --- a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/models/models.ts +++ b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/models/models.ts @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { uint8ArrayToString } from "@azure/core-util"; +import { + FileContents, + createFilePartDescriptor, +} from "../static-helpers/multipartHelpers.js"; import { ErrorModel } from "@azure-rest/core-client"; /** The configuration information for an audio transcription request. */ @@ -10,7 +13,9 @@ export interface AudioTranscriptionOptions { * The audio data to transcribe. This must be the binary content of a file in one of the supported media formats: * flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, webm. */ - file: Uint8Array; + file: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** The optional filename or descriptive identifier to associate with with the audio data. */ filename?: string; /** The requested format of the transcription response data, which will influence the content and detail of the result. */ @@ -46,20 +51,37 @@ export interface AudioTranscriptionOptions { export function audioTranscriptionOptionsSerializer( item: AudioTranscriptionOptions, ): any { - return { - file: uint8ArrayToString(item["file"], "base64"), - filename: item["filename"], - response_format: item["responseFormat"], - language: item["language"], - prompt: item["prompt"], - temperature: item["temperature"], - timestamp_granularities: !item["timestampGranularities"] - ? item["timestampGranularities"] - : item["timestampGranularities"].map((p: any) => { - return p; - }), - model: item["model"], - }; + return [ + createFilePartDescriptor("file", item["file"]), + ...(item["filename"] === undefined + ? [] + : [{ name: "filename", body: item["filename"] }]), + ...(item["responseFormat"] === undefined + ? [] + : [{ name: "response_format", body: item["responseFormat"] }]), + ...(item["language"] === undefined + ? [] + : [{ name: "language", body: item["language"] }]), + ...(item["prompt"] === undefined + ? [] + : [{ name: "prompt", body: item["prompt"] }]), + ...(item["temperature"] === undefined + ? [] + : [{ name: "temperature", body: item["temperature"] }]), + ...(item["timestampGranularities"] === undefined + ? [] + : [ + ...(!item["timestampGranularities"] + ? item["timestampGranularities"] + : item["timestampGranularities"].map((p: any) => { + return p; + }) + ).map((x: unknown) => ({ name: "timestamp_granularities", body: x })), + ]), + ...(item["model"] === undefined + ? [] + : [{ name: "model", body: item["model"] }]), + ]; } /** Defines available options for the underlying response format of output transcription information. */ @@ -204,7 +226,9 @@ export interface AudioTranslationOptions { * The audio data to translate. This must be the binary content of a file in one of the supported media formats: * flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, webm. */ - file: Uint8Array; + file: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** The optional filename or descriptive identifier to associate with with the audio data. */ filename?: string; /** The requested format of the translation response data, which will influence the content and detail of the result. */ @@ -227,14 +251,24 @@ export interface AudioTranslationOptions { export function audioTranslationOptionsSerializer( item: AudioTranslationOptions, ): any { - return { - file: uint8ArrayToString(item["file"], "base64"), - filename: item["filename"], - response_format: item["responseFormat"], - prompt: item["prompt"], - temperature: item["temperature"], - model: item["model"], - }; + return [ + createFilePartDescriptor("file", item["file"]), + ...(item["filename"] === undefined + ? [] + : [{ name: "filename", body: item["filename"] }]), + ...(item["responseFormat"] === undefined + ? [] + : [{ name: "response_format", body: item["responseFormat"] }]), + ...(item["prompt"] === undefined + ? [] + : [{ name: "prompt", body: item["prompt"] }]), + ...(item["temperature"] === undefined + ? [] + : [{ name: "temperature", body: item["temperature"] }]), + ...(item["model"] === undefined + ? [] + : [{ name: "model", body: item["model"] }]), + ]; } /** Defines available options for the underlying response format of output translation information. */ diff --git a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/static-helpers/multipartHelpers.ts b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/static-helpers/multipartHelpers.ts new file mode 100644 index 0000000000..a2770ad2ab --- /dev/null +++ b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/static-helpers/multipartHelpers.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Valid values for the contents of a binary file. + */ +export type FileContents = + | string + | NodeJS.ReadableStream + | ReadableStream + | Uint8Array + | Blob; + +export function createFilePartDescriptor( + partName: string, + fileInput: any, + defaultContentType?: string, +): any { + if (fileInput.contents) { + return { + name: partName, + body: fileInput.contents, + contentType: fileInput.contentType ?? defaultContentType, + filename: fileInput.filename, + }; + } else { + return { + name: partName, + body: fileInput, + contentType: defaultContentType, + }; + } +} diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/review/openai-non-branded.api.md b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/review/openai-non-branded.api.md index 0f4faa9e85..2146cd3a88 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/review/openai-non-branded.api.md +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/review/openai-non-branded.api.md @@ -225,7 +225,11 @@ export interface CreateEmbeddingResponse { // @public export interface CreateFileRequest { - file: Uint8Array; + file: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; purpose: string; } @@ -258,8 +262,16 @@ export interface CreateFineTuningJobRequest { // @public export interface CreateImageEditRequest { - image: Uint8Array; - mask?: Uint8Array; + image: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; + mask?: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; n?: number | null; prompt: string; responseFormat?: ("url" | "b64_json") | null; @@ -280,7 +292,11 @@ export interface CreateImageRequest { // @public export interface CreateImageVariationRequest { - image: Uint8Array; + image: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; n?: number | null; responseFormat?: ("url" | "b64_json") | null; size?: ("256x256" | "512x512" | "1024x1024") | null; @@ -331,7 +347,11 @@ export interface CreateModerationResponse { // @public export interface CreateTranscriptionRequest { - file: Uint8Array; + file: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; language?: string; model: "whisper-1"; prompt?: string; @@ -347,7 +367,11 @@ export interface CreateTranscriptionResponse { // @public export interface CreateTranslationRequest { - file: Uint8Array; + file: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; model: "whisper-1"; prompt?: string; responseFormat?: "json" | "text" | "srt" | "verbose_json" | "vtt"; @@ -425,6 +449,9 @@ export interface ErrorResponse { error: ErrorModel; } +// @public +export type FileContents = string | NodeJS.ReadableStream | ReadableStream | Uint8Array | Blob; + // @public export interface FilesCreateOptionalParams extends OperationOptions { } diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/index.ts index f41cb3f14d..dcaf704688 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/index.ts @@ -3,11 +3,11 @@ import { OpenAIContext } from "../../api/openAIContext.js"; import { AudioTranscriptionsOperations, - getAudioTranscriptionsOperations, + _getAudioTranscriptionsOperations, } from "./transcriptions/index.js"; import { AudioTranslationsOperations, - getAudioTranslationsOperations, + _getAudioTranslationsOperations, } from "./translations/index.js"; /** Interface representing a Audio operations. */ @@ -16,9 +16,9 @@ export interface AudioOperations { transcriptions: AudioTranscriptionsOperations; } -export function getAudioOperations(context: OpenAIContext): AudioOperations { +export function _getAudioOperations(context: OpenAIContext): AudioOperations { return { - translations: getAudioTranslationsOperations(context), - transcriptions: getAudioTranscriptionsOperations(context), + translations: _getAudioTranslationsOperations(context), + transcriptions: _getAudioTranscriptionsOperations(context), }; } diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/transcriptions/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/transcriptions/index.ts index 062c890905..41813af0fa 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/transcriptions/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/transcriptions/index.ts @@ -25,7 +25,7 @@ function _getAudioTranscriptions(context: OpenAIContext) { }; } -export function getAudioTranscriptionsOperations( +export function _getAudioTranscriptionsOperations( context: OpenAIContext, ): AudioTranscriptionsOperations { return { diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/translations/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/translations/index.ts index c7ae7b6c67..6d06341c9c 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/translations/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/audio/translations/index.ts @@ -25,7 +25,7 @@ function _getAudioTranslations(context: OpenAIContext) { }; } -export function getAudioTranslationsOperations( +export function _getAudioTranslationsOperations( context: OpenAIContext, ): AudioTranslationsOperations { return { diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/chat/completions/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/chat/completions/index.ts index dbb7e70f64..ed818cf5a8 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/chat/completions/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/chat/completions/index.ts @@ -25,7 +25,7 @@ function _getChatCompletions(context: OpenAIContext) { }; } -export function getChatCompletionsOperations( +export function _getChatCompletionsOperations( context: OpenAIContext, ): ChatCompletionsOperations { return { diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/chat/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/chat/index.ts index 12a97ef1a8..3831c7943a 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/chat/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/chat/index.ts @@ -3,7 +3,7 @@ import { OpenAIContext } from "../../api/openAIContext.js"; import { ChatCompletionsOperations, - getChatCompletionsOperations, + _getChatCompletionsOperations, } from "./completions/index.js"; /** Interface representing a Chat operations. */ @@ -11,8 +11,8 @@ export interface ChatOperations { completions: ChatCompletionsOperations; } -export function getChatOperations(context: OpenAIContext): ChatOperations { +export function _getChatOperations(context: OpenAIContext): ChatOperations { return { - completions: getChatCompletionsOperations(context), + completions: _getChatCompletionsOperations(context), }; } diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/completions/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/completions/index.ts index 6efdae4c46..0d5ab41cdb 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/completions/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/completions/index.ts @@ -25,7 +25,7 @@ function _getCompletions(context: OpenAIContext) { }; } -export function getCompletionsOperations( +export function _getCompletionsOperations( context: OpenAIContext, ): CompletionsOperations { return { diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/edits/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/edits/index.ts index 68d7f9207b..b6c915d67d 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/edits/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/edits/index.ts @@ -20,7 +20,7 @@ function _getEdits(context: OpenAIContext) { }; } -export function getEditsOperations(context: OpenAIContext): EditsOperations { +export function _getEditsOperations(context: OpenAIContext): EditsOperations { return { ..._getEdits(context), }; diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/embeddings/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/embeddings/index.ts index 6b909fd442..a2b70e3675 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/embeddings/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/embeddings/index.ts @@ -25,7 +25,7 @@ function _getEmbeddings(context: OpenAIContext) { }; } -export function getEmbeddingsOperations( +export function _getEmbeddingsOperations( context: OpenAIContext, ): EmbeddingsOperations { return { diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/files/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/files/index.ts index e640bd46e7..a8f43ab583 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/files/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/files/index.ts @@ -62,7 +62,7 @@ function _getFiles(context: OpenAIContext) { }; } -export function getFilesOperations(context: OpenAIContext): FilesOperations { +export function _getFilesOperations(context: OpenAIContext): FilesOperations { return { ..._getFiles(context), }; diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTunes/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTunes/index.ts index 542bb5ccea..cac76dde05 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTunes/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTunes/index.ts @@ -63,7 +63,7 @@ function _getFineTunes(context: OpenAIContext) { }; } -export function getFineTunesOperations( +export function _getFineTunesOperations( context: OpenAIContext, ): FineTunesOperations { return { diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTuning/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTuning/index.ts index 601585f020..e66fdc8cac 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTuning/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTuning/index.ts @@ -3,7 +3,7 @@ import { OpenAIContext } from "../../api/openAIContext.js"; import { FineTuningJobsOperations, - getFineTuningJobsOperations, + _getFineTuningJobsOperations, } from "./jobs/index.js"; /** Interface representing a FineTuning operations. */ @@ -11,10 +11,10 @@ export interface FineTuningOperations { jobs: FineTuningJobsOperations; } -export function getFineTuningOperations( +export function _getFineTuningOperations( context: OpenAIContext, ): FineTuningOperations { return { - jobs: getFineTuningJobsOperations(context), + jobs: _getFineTuningJobsOperations(context), }; } diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTuning/jobs/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTuning/jobs/index.ts index 09797961a5..c0f109c498 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTuning/jobs/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/fineTuning/jobs/index.ts @@ -76,7 +76,7 @@ function _getFineTuningJobs(context: OpenAIContext) { }; } -export function getFineTuningJobsOperations( +export function _getFineTuningJobsOperations( context: OpenAIContext, ): FineTuningJobsOperations { return { diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/images/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/images/index.ts index 4f537ad7a6..f7b282704f 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/images/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/images/index.ts @@ -45,7 +45,7 @@ function _getImages(context: OpenAIContext) { }; } -export function getImagesOperations(context: OpenAIContext): ImagesOperations { +export function _getImagesOperations(context: OpenAIContext): ImagesOperations { return { ..._getImages(context), }; diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/models/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/models/index.ts index 8fa22fdf98..2c525bdbc3 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/models/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/models/index.ts @@ -41,7 +41,7 @@ function _getModels(context: OpenAIContext) { }; } -export function getModelsOperations(context: OpenAIContext): ModelsOperations { +export function _getModelsOperations(context: OpenAIContext): ModelsOperations { return { ..._getModels(context), }; diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/moderations/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/moderations/index.ts index dbb4874430..8fdff370f1 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/moderations/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/classic/moderations/index.ts @@ -25,7 +25,7 @@ function _getModerations(context: OpenAIContext) { }; } -export function getModerationsOperations( +export function _getModerationsOperations( context: OpenAIContext, ): ModerationsOperations { return { diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/index.ts index 483839f07e..aef13818a1 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/index.ts @@ -1,5 +1,7 @@ // Licensed under the MIT License. +import { FileContents } from "./static-helpers/multipartHelpers.js"; + export { OpenAIClient } from "./openAIClient.js"; export { CreateModerationRequest, @@ -98,3 +100,4 @@ export { ChatCompletionsOperations, FineTuningJobsOperations, } from "./classic/index.js"; +export { FileContents }; diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/models.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/models.ts index c0dd410bd0..f764558bf2 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/models.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/models.ts @@ -1,9 +1,10 @@ // Licensed under the MIT License. import { - uint8ArrayToString, - stringToUint8Array, -} from "@typespec/ts-http-runtime"; + FileContents, + createFilePartDescriptor, +} from "../static-helpers/multipartHelpers.js"; +import { stringToUint8Array } from "@typespec/ts-http-runtime"; /** model interface CreateModerationRequest */ export interface CreateModerationRequest { @@ -346,13 +347,17 @@ export interface CreateImageEditRequest { * The image to edit. Must be a valid PNG file, less than 4MB, and square. If mask is not * provided, image must have transparency, which will be used as the mask. */ - image: Uint8Array; + image: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** * An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where * `image` should be edited. Must be a valid PNG file, less than 4MB, and have the same dimensions * as `image`. */ - mask?: Uint8Array; + mask?: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** The number of images to generate. Must be between 1 and 10. */ n?: number | null; /** The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`. */ @@ -365,17 +370,23 @@ export interface CreateImageEditRequest { export function createImageEditRequestSerializer( item: CreateImageEditRequest, ): any { - return { - prompt: item["prompt"], - image: uint8ArrayToString(item["image"], "base64"), - mask: !item["mask"] - ? item["mask"] - : uint8ArrayToString(item["mask"], "base64"), - n: item["n"], - size: item["size"], - response_format: item["responseFormat"], - user: item["user"], - }; + return [ + { name: "prompt", body: item["prompt"] }, + createFilePartDescriptor("image", item["image"]), + ...(item["mask"] === undefined + ? [] + : [createFilePartDescriptor("mask", item["mask"])]), + ...(item["n"] === undefined ? [] : [{ name: "n", body: item["n"] }]), + ...(item["size"] === undefined + ? [] + : [{ name: "size", body: item["size"] }]), + ...(item["responseFormat"] === undefined + ? [] + : [{ name: "response_format", body: item["responseFormat"] }]), + ...(item["user"] === undefined + ? [] + : [{ name: "user", body: item["user"] }]), + ]; } /** model interface CreateImageVariationRequest */ @@ -384,7 +395,9 @@ export interface CreateImageVariationRequest { * The image to use as the basis for the variation(s). Must be a valid PNG file, less than 4MB, * and square. */ - image: Uint8Array; + image: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** The number of images to generate. Must be between 1 and 10. */ n?: number | null; /** The size of the generated images. Must be one of `256x256`, `512x512`, or `1024x1024`. */ @@ -397,13 +410,19 @@ export interface CreateImageVariationRequest { export function createImageVariationRequestSerializer( item: CreateImageVariationRequest, ): any { - return { - image: uint8ArrayToString(item["image"], "base64"), - n: item["n"], - size: item["size"], - response_format: item["responseFormat"], - user: item["user"], - }; + return [ + createFilePartDescriptor("image", item["image"]), + ...(item["n"] === undefined ? [] : [{ name: "n", body: item["n"] }]), + ...(item["size"] === undefined + ? [] + : [{ name: "size", body: item["size"] }]), + ...(item["responseFormat"] === undefined + ? [] + : [{ name: "response_format", body: item["responseFormat"] }]), + ...(item["user"] === undefined + ? [] + : [{ name: "user", body: item["user"] }]), + ]; } /** model interface ListModelsResponse */ @@ -831,7 +850,9 @@ export interface CreateFileRequest { * * If the `purpose` is set to "fine-tune", the file will be used for fine-tuning. */ - file: Uint8Array; + file: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** * The intended purpose of the uploaded documents. Use "fine-tune" for * [fine-tuning](/docs/api-reference/fine-tuning). This allows us to validate the format of the @@ -841,10 +862,10 @@ export interface CreateFileRequest { } export function createFileRequestSerializer(item: CreateFileRequest): any { - return { - file: uint8ArrayToString(item["file"], "base64"), - purpose: item["purpose"], - }; + return [ + createFilePartDescriptor("file", item["file"]), + { name: "purpose", body: item["purpose"] }, + ]; } /** model interface DeleteFileResponse */ @@ -2049,7 +2070,9 @@ export interface CreateTranslationRequest { * The audio file object (not file name) to translate, in one of these formats: flac, mp3, mp4, * mpeg, mpga, m4a, ogg, wav, or webm. */ - file: Uint8Array; + file: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** ID of the model to use. Only `whisper-1` is currently available. */ model: "whisper-1"; /** @@ -2074,13 +2097,19 @@ export interface CreateTranslationRequest { export function createTranslationRequestSerializer( item: CreateTranslationRequest, ): any { - return { - file: uint8ArrayToString(item["file"], "base64"), - model: item["model"], - prompt: item["prompt"], - response_format: item["responseFormat"], - temperature: item["temperature"], - }; + return [ + createFilePartDescriptor("file", item["file"]), + { name: "model", body: item["model"] }, + ...(item["prompt"] === undefined + ? [] + : [{ name: "prompt", body: item["prompt"] }]), + ...(item["responseFormat"] === undefined + ? [] + : [{ name: "response_format", body: item["responseFormat"] }]), + ...(item["temperature"] === undefined + ? [] + : [{ name: "temperature", body: item["temperature"] }]), + ]; } /** model interface CreateTranslationResponse */ @@ -2102,7 +2131,9 @@ export interface CreateTranscriptionRequest { * The audio file object (not file name) to transcribe, in one of these formats: flac, mp3, mp4, * mpeg, mpga, m4a, ogg, wav, or webm. */ - file: Uint8Array; + file: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; /** ID of the model to use. Only `whisper-1` is currently available. */ model: "whisper-1"; /** @@ -2133,14 +2164,22 @@ export interface CreateTranscriptionRequest { export function createTranscriptionRequestSerializer( item: CreateTranscriptionRequest, ): any { - return { - file: uint8ArrayToString(item["file"], "base64"), - model: item["model"], - prompt: item["prompt"], - response_format: item["responseFormat"], - temperature: item["temperature"], - language: item["language"], - }; + return [ + createFilePartDescriptor("file", item["file"]), + { name: "model", body: item["model"] }, + ...(item["prompt"] === undefined + ? [] + : [{ name: "prompt", body: item["prompt"] }]), + ...(item["responseFormat"] === undefined + ? [] + : [{ name: "response_format", body: item["responseFormat"] }]), + ...(item["temperature"] === undefined + ? [] + : [{ name: "temperature", body: item["temperature"] }]), + ...(item["language"] === undefined + ? [] + : [{ name: "language", body: item["language"] }]), + ]; } /** model interface CreateTranscriptionResponse */ diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/openAIClient.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/openAIClient.ts index 9afbc19c16..bf87a0def1 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/openAIClient.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/openAIClient.ts @@ -1,37 +1,37 @@ // Licensed under the MIT License. import { - getModerationsOperations, + _getModerationsOperations, ModerationsOperations, } from "./classic/moderations/index.js"; import { - getImagesOperations, + _getImagesOperations, ImagesOperations, } from "./classic/images/index.js"; import { - getModelsOperations, + _getModelsOperations, ModelsOperations, } from "./classic/models/index.js"; import { - getFineTunesOperations, + _getFineTunesOperations, FineTunesOperations, } from "./classic/fineTunes/index.js"; -import { getFilesOperations, FilesOperations } from "./classic/files/index.js"; +import { _getFilesOperations, FilesOperations } from "./classic/files/index.js"; import { - getEmbeddingsOperations, + _getEmbeddingsOperations, EmbeddingsOperations, } from "./classic/embeddings/index.js"; -import { getEditsOperations, EditsOperations } from "./classic/edits/index.js"; +import { _getEditsOperations, EditsOperations } from "./classic/edits/index.js"; import { - getCompletionsOperations, + _getCompletionsOperations, CompletionsOperations, } from "./classic/completions/index.js"; import { - getFineTuningOperations, + _getFineTuningOperations, FineTuningOperations, } from "./classic/fineTuning/index.js"; -import { getChatOperations, ChatOperations } from "./classic/chat/index.js"; -import { getAudioOperations, AudioOperations } from "./classic/audio/index.js"; +import { _getChatOperations, ChatOperations } from "./classic/chat/index.js"; +import { _getAudioOperations, AudioOperations } from "./classic/audio/index.js"; import { createOpenAI, OpenAIContext, @@ -60,17 +60,17 @@ export class OpenAIClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.moderations = getModerationsOperations(this._client); - this.images = getImagesOperations(this._client); - this.models = getModelsOperations(this._client); - this.fineTunes = getFineTunesOperations(this._client); - this.files = getFilesOperations(this._client); - this.embeddings = getEmbeddingsOperations(this._client); - this.edits = getEditsOperations(this._client); - this.completions = getCompletionsOperations(this._client); - this.fineTuning = getFineTuningOperations(this._client); - this.chat = getChatOperations(this._client); - this.audio = getAudioOperations(this._client); + this.moderations = _getModerationsOperations(this._client); + this.images = _getImagesOperations(this._client); + this.models = _getModelsOperations(this._client); + this.fineTunes = _getFineTunesOperations(this._client); + this.files = _getFilesOperations(this._client); + this.embeddings = _getEmbeddingsOperations(this._client); + this.edits = _getEditsOperations(this._client); + this.completions = _getCompletionsOperations(this._client); + this.fineTuning = _getFineTuningOperations(this._client); + this.chat = _getChatOperations(this._client); + this.audio = _getAudioOperations(this._client); } /** The operation groups for moderations */ diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/static-helpers/multipartHelpers.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/static-helpers/multipartHelpers.ts new file mode 100644 index 0000000000..e704928cf9 --- /dev/null +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/static-helpers/multipartHelpers.ts @@ -0,0 +1,32 @@ +// Licensed under the MIT License. + +/** + * Valid values for the contents of a binary file. + */ +export type FileContents = + | string + | NodeJS.ReadableStream + | ReadableStream + | Uint8Array + | Blob; + +export function createFilePartDescriptor( + partName: string, + fileInput: any, + defaultContentType?: string, +): any { + if (fileInput.contents) { + return { + name: partName, + body: fileInput.contents, + contentType: fileInput.contentType ?? defaultContentType, + filename: fileInput.filename, + }; + } else { + return { + name: partName, + body: fileInput, + contentType: defaultContentType, + }; + } +} diff --git a/packages/typespec-test/test/overloads_modular/generated/typespec-ts/src/classic/fooOperations/index.ts b/packages/typespec-test/test/overloads_modular/generated/typespec-ts/src/classic/fooOperations/index.ts index f0f6ecebc7..9da9818bab 100644 --- a/packages/typespec-test/test/overloads_modular/generated/typespec-ts/src/classic/fooOperations/index.ts +++ b/packages/typespec-test/test/overloads_modular/generated/typespec-ts/src/classic/fooOperations/index.ts @@ -38,7 +38,7 @@ function _getFooOperations(context: WidgetManagerContext) { }; } -export function getFooOperationsOperations( +export function _getFooOperationsOperations( context: WidgetManagerContext, ): FooOperationsOperations { return { diff --git a/packages/typespec-test/test/overloads_modular/generated/typespec-ts/src/widgetManagerClient.ts b/packages/typespec-test/test/overloads_modular/generated/typespec-ts/src/widgetManagerClient.ts index 6ae5b2b9dc..2a2eed0764 100644 --- a/packages/typespec-test/test/overloads_modular/generated/typespec-ts/src/widgetManagerClient.ts +++ b/packages/typespec-test/test/overloads_modular/generated/typespec-ts/src/widgetManagerClient.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { - getFooOperationsOperations, + _getFooOperationsOperations, FooOperationsOperations, } from "./classic/fooOperations/index.js"; import { @@ -34,7 +34,7 @@ export class WidgetManagerClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.fooOperations = getFooOperationsOperations(this._client); + this.fooOperations = _getFooOperationsOperations(this._client); } /** The operation groups for fooOperations */ diff --git a/packages/typespec-test/test/parametrizedHost/generated/typespec-ts/src/classic/confidentialLedger/index.ts b/packages/typespec-test/test/parametrizedHost/generated/typespec-ts/src/classic/confidentialLedger/index.ts index f5bae5a284..5372dde9eb 100644 --- a/packages/typespec-test/test/parametrizedHost/generated/typespec-ts/src/classic/confidentialLedger/index.ts +++ b/packages/typespec-test/test/parametrizedHost/generated/typespec-ts/src/classic/confidentialLedger/index.ts @@ -22,7 +22,7 @@ function _getConfidentialLedger(context: ParametrizedHostContext) { }; } -export function getConfidentialLedgerOperations( +export function _getConfidentialLedgerOperations( context: ParametrizedHostContext, ): ConfidentialLedgerOperations { return { diff --git a/packages/typespec-test/test/parametrizedHost/generated/typespec-ts/src/parametrizedHostClient.ts b/packages/typespec-test/test/parametrizedHost/generated/typespec-ts/src/parametrizedHostClient.ts index fe76fef84e..4b3e13ef12 100644 --- a/packages/typespec-test/test/parametrizedHost/generated/typespec-ts/src/parametrizedHostClient.ts +++ b/packages/typespec-test/test/parametrizedHost/generated/typespec-ts/src/parametrizedHostClient.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { - getConfidentialLedgerOperations, + _getConfidentialLedgerOperations, ConfidentialLedgerOperations, } from "./classic/confidentialLedger/index.js"; import { @@ -34,7 +34,7 @@ export class ParametrizedHostClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.confidentialLedger = getConfidentialLedgerOperations(this._client); + this.confidentialLedger = _getConfidentialLedgerOperations(this._client); } /** The operation groups for confidentialLedger */ diff --git a/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/classic/schemaOperations/index.ts b/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/classic/schemaOperations/index.ts index b9eec85ac1..c3ead4d0d6 100644 --- a/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/classic/schemaOperations/index.ts +++ b/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/classic/schemaOperations/index.ts @@ -113,7 +113,7 @@ function _getSchemaOperations(context: SchemaRegistryContext) { }; } -export function getSchemaOperationsOperations( +export function _getSchemaOperationsOperations( context: SchemaRegistryContext, ): SchemaOperationsOperations { return { diff --git a/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/schemaRegistryClient.ts b/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/schemaRegistryClient.ts index acd2227947..747fc5f8a9 100644 --- a/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/schemaRegistryClient.ts +++ b/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/schemaRegistryClient.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { - getSchemaOperationsOperations, + _getSchemaOperationsOperations, SchemaOperationsOperations, } from "./classic/schemaOperations/index.js"; import { @@ -35,7 +35,7 @@ export class SchemaRegistryClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.schemaOperations = getSchemaOperationsOperations(this._client); + this.schemaOperations = _getSchemaOperationsOperations(this._client); } /** The operation groups for schemaOperations */ diff --git a/packages/typespec-test/test/spread/generated/typespec-ts/src/classic/a/index.ts b/packages/typespec-test/test/spread/generated/typespec-ts/src/classic/a/index.ts index 6af28a81ed..78895387da 100644 --- a/packages/typespec-test/test/spread/generated/typespec-ts/src/classic/a/index.ts +++ b/packages/typespec-test/test/spread/generated/typespec-ts/src/classic/a/index.ts @@ -54,7 +54,7 @@ function _getA(context: DemoServiceContext) { }; } -export function getAOperations(context: DemoServiceContext): AOperations { +export function _getAOperations(context: DemoServiceContext): AOperations { return { ..._getA(context), }; diff --git a/packages/typespec-test/test/spread/generated/typespec-ts/src/demoServiceClient.ts b/packages/typespec-test/test/spread/generated/typespec-ts/src/demoServiceClient.ts index b7281c3bb4..59bd36517c 100644 --- a/packages/typespec-test/test/spread/generated/typespec-ts/src/demoServiceClient.ts +++ b/packages/typespec-test/test/spread/generated/typespec-ts/src/demoServiceClient.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { getAOperations, AOperations } from "./classic/a/index.js"; +import { _getAOperations, AOperations } from "./classic/a/index.js"; import { createDemoService, DemoServiceContext, @@ -29,7 +29,7 @@ export class DemoServiceClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.a = getAOperations(this._client); + this.a = _getAOperations(this._client); } /** The operation groups for a */ diff --git a/packages/typespec-test/test/todo_non_branded/generated/openapi/openapi.json b/packages/typespec-test/test/todo_non_branded/generated/openapi/openapi.json index eac5b5010a..56dbbe9bec 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/openapi/openapi.json +++ b/packages/typespec-test/test/todo_non_branded/generated/openapi/openapi.json @@ -51,7 +51,7 @@ } }, "post": { - "operationId": "TodoItems_create", + "operationId": "TodoItems_createJson_TodoItems_createForm", "parameters": [], "responses": { "200": { @@ -116,6 +116,19 @@ "item" ] } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/ToDoItemMultipartRequest" + }, + "encoding": { + "item": { + "contentType": "application/json" + }, + "attachments": { + "contentType": "*/*" + } + } } } } @@ -317,7 +330,7 @@ } }, "post": { - "operationId": "Attachments_createAttachment", + "operationId": "Attachments_createJsonAttachment_Attachments_createFileAttachment", "parameters": [ { "name": "itemId", @@ -372,6 +385,16 @@ "schema": { "$ref": "#/components/schemas/TodoAttachment" } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/FileAttachmentMultipartRequest" + }, + "encoding": { + "contents": { + "contentType": "*/*" + } + } } } } @@ -503,6 +526,18 @@ } } }, + "FileAttachmentMultipartRequest": { + "type": "object", + "properties": { + "contents": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "contents" + ] + }, "Standard4XXResponse": { "type": "object", "allOf": [ @@ -521,17 +556,25 @@ ], "description": "Something is wrong with me." }, - "TodoAttachment": { - "anyOf": [ - { - "$ref": "#/components/schemas/TodoFileAttachment" + "ToDoItemMultipartRequest": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/TodoItemCreate" }, - { - "$ref": "#/components/schemas/TodoUrlAttachment" + "attachments": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } } + }, + "required": [ + "item" ] }, - "TodoFileAttachment": { + "TodoAttachment": { "type": "object", "required": [ "filename", @@ -803,24 +846,6 @@ } ] }, - "TodoUrlAttachment": { - "type": "object", - "required": [ - "description", - "url" - ], - "properties": { - "description": { - "type": "string", - "description": "A description of the URL" - }, - "url": { - "type": "string", - "format": "uri", - "description": "The url" - } - } - }, "User": { "type": "object", "required": [ diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/review/todo-non-branded.api.md b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/review/todo-non-branded.api.md index 14f1fe41da..81dff24324 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/review/todo-non-branded.api.md +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/review/todo-non-branded.api.md @@ -20,6 +20,19 @@ export type ContinuablePage = TPage & { continuationToken?: string; }; +// @public +export interface FileAttachmentMultipartRequest { + // (undocumented) + contents: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; +} + +// @public +export type FileContents = string | NodeJS.ReadableStream | ReadableStream | Uint8Array | Blob; + // @public export interface InvalidTodoItem extends ApiError { } @@ -63,7 +76,11 @@ export interface Standard5XXResponse extends ApiError { } // @public -export type TodoAttachment = TodoFileAttachment | TodoUrlAttachment; +export interface TodoAttachment { + contents: Uint8Array; + filename: string; + mediaType: string; +} // @public (undocumented) export class TodoClient { @@ -77,13 +94,6 @@ export class TodoClient { export interface TodoClientOptionalParams extends ClientOptions { } -// @public -export interface TodoFileAttachment { - contents: Uint8Array; - filename: string; - mediaType: string; -} - // @public export interface TodoItem { assignedTo?: number; @@ -101,6 +111,18 @@ export interface TodoItem { readonly updatedAt: Date; } +// @public +export interface ToDoItemMultipartRequest { + // (undocumented) + attachments?: Array; + // (undocumented) + item: TodoItem; +} + // @public export interface TodoItemPatch { assignedTo?: number | null; @@ -110,7 +132,11 @@ export interface TodoItemPatch { } // @public -export interface TodoItemsAttachmentsCreateAttachmentOptionalParams extends OperationOptions { +export interface TodoItemsAttachmentsCreateFileAttachmentOptionalParams extends OperationOptions { +} + +// @public +export interface TodoItemsAttachmentsCreateJsonAttachmentOptionalParams extends OperationOptions { } // @public @@ -120,13 +146,19 @@ export interface TodoItemsAttachmentsListOptionalParams extends OperationOptions // @public export interface TodoItemsAttachmentsOperations { // (undocumented) - createAttachment: (itemId: number, contents: TodoAttachment, options?: TodoItemsAttachmentsCreateAttachmentOptionalParams) => Promise; + createFileAttachment: (itemId: number, body: FileAttachmentMultipartRequest, options?: TodoItemsAttachmentsCreateFileAttachmentOptionalParams) => Promise; + // (undocumented) + createJsonAttachment: (itemId: number, contents: TodoAttachment, options?: TodoItemsAttachmentsCreateJsonAttachmentOptionalParams) => Promise; // (undocumented) list: (itemId: number, options?: TodoItemsAttachmentsListOptionalParams) => PagedAsyncIterableIterator; } // @public -export interface TodoItemsCreateOptionalParams extends OperationOptions { +export interface TodoItemsCreateFormOptionalParams extends OperationOptions { +} + +// @public +export interface TodoItemsCreateJsonOptionalParams extends OperationOptions { // (undocumented) attachments?: TodoAttachment[]; } @@ -150,7 +182,20 @@ export interface TodoItemsOperations { // (undocumented) attachments: TodoItemsAttachmentsOperations; // (undocumented) - create: (item: TodoItem, options?: TodoItemsCreateOptionalParams) => Promise<{ + createForm: (body: ToDoItemMultipartRequest, options?: TodoItemsCreateFormOptionalParams) => Promise<{ + id: number; + title: string; + createdBy: number; + assignedTo?: number; + description?: string; + status: "NotStarted" | "InProgress" | "Completed"; + createdAt: Date; + updatedAt: Date; + completedAt?: Date; + labels?: TodoLabels; + }>; + // (undocumented) + createJson: (item: TodoItem, options?: TodoItemsCreateJsonOptionalParams) => Promise<{ id: number; title: string; createdBy: number; @@ -217,12 +262,6 @@ export interface TodoPage { totalSize: number; } -// @public -export interface TodoUrlAttachment { - description: string; - url: string; -} - // @public export interface User { email: string; diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/index.ts index 3054d820a9..28b6720915 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/index.ts @@ -1,12 +1,14 @@ // Licensed under the MIT License. export { - TodoItemsAttachmentsCreateAttachmentOptionalParams, + TodoItemsAttachmentsCreateFileAttachmentOptionalParams, + TodoItemsAttachmentsCreateJsonAttachmentOptionalParams, TodoItemsAttachmentsListOptionalParams, TodoItemsDeleteOptionalParams, TodoItemsUpdateOptionalParams, TodoItemsGetOptionalParams, - TodoItemsCreateOptionalParams, + TodoItemsCreateFormOptionalParams, + TodoItemsCreateJsonOptionalParams, TodoItemsListOptionalParams, UsersCreateOptionalParams, } from "./options.js"; diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/options.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/options.ts index a3c2012467..4ce34ed381 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/options.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/options.ts @@ -4,7 +4,11 @@ import { TodoAttachment } from "../models/models.js"; import { OperationOptions } from "@typespec/ts-http-runtime"; /** Optional parameters. */ -export interface TodoItemsAttachmentsCreateAttachmentOptionalParams +export interface TodoItemsAttachmentsCreateFileAttachmentOptionalParams + extends OperationOptions {} + +/** Optional parameters. */ +export interface TodoItemsAttachmentsCreateJsonAttachmentOptionalParams extends OperationOptions {} /** Optional parameters. */ @@ -21,7 +25,10 @@ export interface TodoItemsUpdateOptionalParams extends OperationOptions {} export interface TodoItemsGetOptionalParams extends OperationOptions {} /** Optional parameters. */ -export interface TodoItemsCreateOptionalParams extends OperationOptions { +export interface TodoItemsCreateFormOptionalParams extends OperationOptions {} + +/** Optional parameters. */ +export interface TodoItemsCreateJsonOptionalParams extends OperationOptions { attachments?: TodoAttachment[]; } diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/attachments/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/attachments/index.ts index 8b75e369e9..2523b31c0e 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/attachments/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/attachments/index.ts @@ -2,7 +2,8 @@ import { TodoContext as Client, - TodoItemsAttachmentsCreateAttachmentOptionalParams, + TodoItemsAttachmentsCreateFileAttachmentOptionalParams, + TodoItemsAttachmentsCreateJsonAttachmentOptionalParams, TodoItemsAttachmentsListOptionalParams, } from "../../index.js"; import { @@ -15,6 +16,8 @@ import { standard5XXResponseDeserializer, TodoAttachment, todoAttachmentSerializer, + FileAttachmentMultipartRequest, + fileAttachmentMultipartRequestSerializer, } from "../../../models/models.js"; import { PagedAsyncIterableIterator, @@ -27,11 +30,69 @@ import { operationOptionsToRequestParameters, } from "@typespec/ts-http-runtime"; -export function _createAttachmentSend( +export function _createFileAttachmentSend( + context: Client, + itemId: number, + body: FileAttachmentMultipartRequest, + options: TodoItemsAttachmentsCreateFileAttachmentOptionalParams = { + requestOptions: {}, + }, +): StreamableMethod { + return context + .path("/items/{itemId}/attachments", itemId) + .post({ + ...operationOptionsToRequestParameters(options), + contentType: "multipart/form-data", + headers: { + accept: "application/json", + ...options.requestOptions?.headers, + }, + body: fileAttachmentMultipartRequestSerializer(body), + }); +} + +export async function _createFileAttachmentDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["204"]; + if (!expectedStatuses.includes(result.status)) { + const error = createRestError(result); + const statusCode = Number.parseInt(result.status); + if (statusCode === 404) { + error.details = notFoundErrorResponseDeserializer(result.body); + } else if (statusCode >= 400 && statusCode <= 499) { + error.details = standard4XXResponseDeserializer(result.body); + } else if (statusCode >= 500 && statusCode <= 599) { + error.details = standard5XXResponseDeserializer(result.body); + } + throw error; + } + + return; +} + +export async function createFileAttachment( + context: Client, + itemId: number, + body: FileAttachmentMultipartRequest, + options: TodoItemsAttachmentsCreateFileAttachmentOptionalParams = { + requestOptions: {}, + }, +): Promise { + const result = await _createFileAttachmentSend( + context, + itemId, + body, + options, + ); + return _createFileAttachmentDeserialize(result); +} + +export function _createJsonAttachmentSend( context: Client, itemId: number, contents: TodoAttachment, - options: TodoItemsAttachmentsCreateAttachmentOptionalParams = { + options: TodoItemsAttachmentsCreateJsonAttachmentOptionalParams = { requestOptions: {}, }, ): StreamableMethod { @@ -48,7 +109,7 @@ export function _createAttachmentSend( }); } -export async function _createAttachmentDeserialize( +export async function _createJsonAttachmentDeserialize( result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; @@ -68,21 +129,21 @@ export async function _createAttachmentDeserialize( return; } -export async function createAttachment( +export async function createJsonAttachment( context: Client, itemId: number, contents: TodoAttachment, - options: TodoItemsAttachmentsCreateAttachmentOptionalParams = { + options: TodoItemsAttachmentsCreateJsonAttachmentOptionalParams = { requestOptions: {}, }, ): Promise { - const result = await _createAttachmentSend( + const result = await _createJsonAttachmentSend( context, itemId, contents, options, ); - return _createAttachmentDeserialize(result); + return _createJsonAttachmentDeserialize(result); } export function _listSend( diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/index.ts index 84583a5503..4ad7c58a7c 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/index.ts @@ -2,7 +2,8 @@ import { TodoContext as Client, - TodoItemsCreateOptionalParams, + TodoItemsCreateFormOptionalParams, + TodoItemsCreateJsonOptionalParams, TodoItemsDeleteOptionalParams, TodoItemsGetOptionalParams, TodoItemsListOptionalParams, @@ -23,7 +24,10 @@ import { standard4XXResponseDeserializer, standard5XXResponseDeserializer, todoAttachmentArraySerializer, - _createResponseDeserializer, + _createJsonResponseDeserializer, + ToDoItemMultipartRequest, + toDoItemMultipartRequestSerializer, + _createFormResponseDeserializer, _getResponseDeserializer, _updateResponseDeserializer, } from "../../models/models.js"; @@ -211,10 +215,79 @@ export async function get( return _getDeserialize(result); } -export function _createSend( +export function _createFormSend( + context: Client, + body: ToDoItemMultipartRequest, + options: TodoItemsCreateFormOptionalParams = { requestOptions: {} }, +): StreamableMethod { + return context + .path("/items") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: "multipart/form-data", + headers: { + accept: "application/json", + ...options.requestOptions?.headers, + }, + body: toDoItemMultipartRequestSerializer(body), + }); +} + +export async function _createFormDeserialize( + result: PathUncheckedResponse, +): Promise<{ + id: number; + title: string; + createdBy: number; + assignedTo?: number; + description?: string; + status: "NotStarted" | "InProgress" | "Completed"; + createdAt: Date; + updatedAt: Date; + completedAt?: Date; + labels?: TodoLabels; +}> { + const expectedStatuses = ["200"]; + if (!expectedStatuses.includes(result.status)) { + const error = createRestError(result); + const statusCode = Number.parseInt(result.status); + if (statusCode === 422) { + error.details = invalidTodoItemDeserializer(result.body); + } else if (statusCode >= 400 && statusCode <= 499) { + error.details = standard4XXResponseDeserializer(result.body); + } else if (statusCode >= 500 && statusCode <= 599) { + error.details = standard5XXResponseDeserializer(result.body); + } + throw error; + } + + return _createFormResponseDeserializer(result.body); +} + +export async function createForm( + context: Client, + body: ToDoItemMultipartRequest, + options: TodoItemsCreateFormOptionalParams = { requestOptions: {} }, +): Promise<{ + id: number; + title: string; + createdBy: number; + assignedTo?: number; + description?: string; + status: "NotStarted" | "InProgress" | "Completed"; + createdAt: Date; + updatedAt: Date; + completedAt?: Date; + labels?: TodoLabels; +}> { + const result = await _createFormSend(context, body, options); + return _createFormDeserialize(result); +} + +export function _createJsonSend( context: Client, item: TodoItem, - options: TodoItemsCreateOptionalParams = { requestOptions: {} }, + options: TodoItemsCreateJsonOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/items") @@ -234,7 +307,7 @@ export function _createSend( }); } -export async function _createDeserialize( +export async function _createJsonDeserialize( result: PathUncheckedResponse, ): Promise<{ id: number; @@ -262,13 +335,13 @@ export async function _createDeserialize( throw error; } - return _createResponseDeserializer(result.body); + return _createJsonResponseDeserializer(result.body); } -export async function create( +export async function createJson( context: Client, item: TodoItem, - options: TodoItemsCreateOptionalParams = { requestOptions: {} }, + options: TodoItemsCreateJsonOptionalParams = { requestOptions: {} }, ): Promise<{ id: number; title: string; @@ -281,8 +354,8 @@ export async function create( completedAt?: Date; labels?: TodoLabels; }> { - const result = await _createSend(context, item, options); - return _createDeserialize(result); + const result = await _createJsonSend(context, item, options); + return _createJsonDeserialize(result); } export function _listSend( diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/users/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/users/index.ts index d2c58516d7..41a80e1402 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/users/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/users/index.ts @@ -6,7 +6,7 @@ import { standard5XXResponseDeserializer, User, userSerializer, - _createResponse1Deserializer, + _createResponseDeserializer, } from "../../models/models.js"; import { userExistsResponseDeserializer, @@ -61,7 +61,7 @@ export async function _createDeserialize( throw error; } - return _createResponse1Deserializer(result.body); + return _createResponseDeserializer(result.body); } export async function create( diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/todoItems/attachments/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/todoItems/attachments/index.ts index a475a2a637..d0599c3ceb 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/todoItems/attachments/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/todoItems/attachments/index.ts @@ -2,22 +2,32 @@ import { TodoContext } from "../../../api/todoContext.js"; import { - createAttachment, + createFileAttachment, + createJsonAttachment, list, } from "../../../api/todoItems/attachments/index.js"; -import { TodoAttachment } from "../../../models/models.js"; +import { + TodoAttachment, + FileAttachmentMultipartRequest, +} from "../../../models/models.js"; import { PagedAsyncIterableIterator } from "../../../static-helpers/pagingHelpers.js"; import { - TodoItemsAttachmentsCreateAttachmentOptionalParams, + TodoItemsAttachmentsCreateFileAttachmentOptionalParams, + TodoItemsAttachmentsCreateJsonAttachmentOptionalParams, TodoItemsAttachmentsListOptionalParams, } from "../../../api/options.js"; /** Interface representing a TodoItemsAttachments operations. */ export interface TodoItemsAttachmentsOperations { - createAttachment: ( + createFileAttachment: ( + itemId: number, + body: FileAttachmentMultipartRequest, + options?: TodoItemsAttachmentsCreateFileAttachmentOptionalParams, + ) => Promise; + createJsonAttachment: ( itemId: number, contents: TodoAttachment, - options?: TodoItemsAttachmentsCreateAttachmentOptionalParams, + options?: TodoItemsAttachmentsCreateJsonAttachmentOptionalParams, ) => Promise; list: ( itemId: number, @@ -27,17 +37,22 @@ export interface TodoItemsAttachmentsOperations { function _getTodoItemsAttachments(context: TodoContext) { return { - createAttachment: ( + createFileAttachment: ( + itemId: number, + body: FileAttachmentMultipartRequest, + options?: TodoItemsAttachmentsCreateFileAttachmentOptionalParams, + ) => createFileAttachment(context, itemId, body, options), + createJsonAttachment: ( itemId: number, contents: TodoAttachment, - options?: TodoItemsAttachmentsCreateAttachmentOptionalParams, - ) => createAttachment(context, itemId, contents, options), + options?: TodoItemsAttachmentsCreateJsonAttachmentOptionalParams, + ) => createJsonAttachment(context, itemId, contents, options), list: (itemId: number, options?: TodoItemsAttachmentsListOptionalParams) => list(context, itemId, options), }; } -export function getTodoItemsAttachmentsOperations( +export function _getTodoItemsAttachmentsOperations( context: TodoContext, ): TodoItemsAttachmentsOperations { return { diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/todoItems/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/todoItems/index.ts index 57e36d8b25..9cb34a03a3 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/todoItems/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/todoItems/index.ts @@ -5,22 +5,28 @@ import { $delete, update, get, - create, + createForm, + createJson, list, } from "../../api/todoItems/index.js"; import { TodoItemPatch } from "../../models/todoItems/models.js"; -import { TodoItem, TodoLabels } from "../../models/models.js"; +import { + TodoItem, + TodoLabels, + ToDoItemMultipartRequest, +} from "../../models/models.js"; import { PagedAsyncIterableIterator } from "../../static-helpers/pagingHelpers.js"; import { TodoItemsDeleteOptionalParams, TodoItemsUpdateOptionalParams, TodoItemsGetOptionalParams, - TodoItemsCreateOptionalParams, + TodoItemsCreateFormOptionalParams, + TodoItemsCreateJsonOptionalParams, TodoItemsListOptionalParams, } from "../../api/options.js"; import { TodoItemsAttachmentsOperations, - getTodoItemsAttachmentsOperations, + _getTodoItemsAttachmentsOperations, } from "./attachments/index.js"; /** Interface representing a TodoItems operations. */ @@ -65,9 +71,24 @@ export interface TodoItemsOperations { completedAt?: Date; labels?: TodoLabels; }>; - create: ( + createForm: ( + body: ToDoItemMultipartRequest, + options?: TodoItemsCreateFormOptionalParams, + ) => Promise<{ + id: number; + title: string; + createdBy: number; + assignedTo?: number; + description?: string; + status: "NotStarted" | "InProgress" | "Completed"; + createdAt: Date; + updatedAt: Date; + completedAt?: Date; + labels?: TodoLabels; + }>; + createJson: ( item: TodoItem, - options?: TodoItemsCreateOptionalParams, + options?: TodoItemsCreateJsonOptionalParams, ) => Promise<{ id: number; title: string; @@ -97,17 +118,21 @@ function _getTodoItems(context: TodoContext) { ) => update(context, id, patch, options), get: (id: number, options?: TodoItemsGetOptionalParams) => get(context, id, options), - create: (item: TodoItem, options?: TodoItemsCreateOptionalParams) => - create(context, item, options), + createForm: ( + body: ToDoItemMultipartRequest, + options?: TodoItemsCreateFormOptionalParams, + ) => createForm(context, body, options), + createJson: (item: TodoItem, options?: TodoItemsCreateJsonOptionalParams) => + createJson(context, item, options), list: (options?: TodoItemsListOptionalParams) => list(context, options), }; } -export function getTodoItemsOperations( +export function _getTodoItemsOperations( context: TodoContext, ): TodoItemsOperations { return { ..._getTodoItems(context), - attachments: getTodoItemsAttachmentsOperations(context), + attachments: _getTodoItemsAttachmentsOperations(context), }; } diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/users/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/users/index.ts index 8af106bb94..9d1b286568 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/users/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/classic/users/index.ts @@ -25,7 +25,7 @@ function _getUsers(context: TodoContext) { }; } -export function getUsersOperations(context: TodoContext): UsersOperations { +export function _getUsersOperations(context: TodoContext): UsersOperations { return { ..._getUsers(context), }; diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/index.ts index e1fa73ba71..1eb9e59758 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/index.ts @@ -1,5 +1,6 @@ // Licensed under the MIT License. +import { FileContents } from "./static-helpers/multipartHelpers.js"; import { PageSettings, ContinuablePage, @@ -14,9 +15,9 @@ export { Standard4XXResponse, ApiError, Standard5XXResponse, - TodoFileAttachment, - TodoUrlAttachment, TodoAttachment, + ToDoItemMultipartRequest, + FileAttachmentMultipartRequest, User, } from "./models/index.js"; export { @@ -31,12 +32,14 @@ export { InvalidUserResponse, } from "./models/users/index.js"; export { - TodoItemsAttachmentsCreateAttachmentOptionalParams, + TodoItemsAttachmentsCreateFileAttachmentOptionalParams, + TodoItemsAttachmentsCreateJsonAttachmentOptionalParams, TodoItemsAttachmentsListOptionalParams, TodoItemsDeleteOptionalParams, TodoItemsUpdateOptionalParams, TodoItemsGetOptionalParams, - TodoItemsCreateOptionalParams, + TodoItemsCreateFormOptionalParams, + TodoItemsCreateJsonOptionalParams, TodoItemsListOptionalParams, UsersCreateOptionalParams, TodoClientOptionalParams, @@ -47,3 +50,4 @@ export { TodoItemsAttachmentsOperations, } from "./classic/index.js"; export { PageSettings, ContinuablePage, PagedAsyncIterableIterator }; +export { FileContents }; diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/index.ts index ec945422a3..6e13de2781 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/index.ts @@ -7,8 +7,8 @@ export { Standard4XXResponse, ApiError, Standard5XXResponse, - TodoFileAttachment, - TodoUrlAttachment, TodoAttachment, + ToDoItemMultipartRequest, + FileAttachmentMultipartRequest, User, } from "./models.js"; diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/models.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/models.ts index 77d8fec9f9..f28fff697f 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/models.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/models.ts @@ -1,5 +1,9 @@ // Licensed under the MIT License. +import { + FileContents, + createFilePartDescriptor, +} from "../static-helpers/multipartHelpers.js"; import { uint8ArrayToString, stringToUint8Array, @@ -161,8 +165,8 @@ export function standard5XXResponseDeserializer( }; } -/** model interface TodoFileAttachment */ -export interface TodoFileAttachment { +/** model interface TodoAttachment */ +export interface TodoAttachment { /** The file name of the attachment */ filename: string; /** The media type of the attachment */ @@ -171,7 +175,7 @@ export interface TodoFileAttachment { contents: Uint8Array; } -export function todoFileAttachmentSerializer(item: TodoFileAttachment): any { +export function todoAttachmentSerializer(item: TodoAttachment): any { return { filename: item["filename"], mediaType: item["mediaType"], @@ -179,7 +183,7 @@ export function todoFileAttachmentSerializer(item: TodoFileAttachment): any { }; } -export function todoFileAttachmentDeserializer(item: any): TodoFileAttachment { +export function todoAttachmentDeserializer(item: any): TodoAttachment { return { filename: item["filename"], mediaType: item["mediaType"], @@ -190,25 +194,6 @@ export function todoFileAttachmentDeserializer(item: any): TodoFileAttachment { }; } -/** model interface TodoUrlAttachment */ -export interface TodoUrlAttachment { - /** A description of the URL */ - description: string; - /** The url */ - url: string; -} - -export function todoUrlAttachmentSerializer(item: TodoUrlAttachment): any { - return { description: item["description"], url: item["url"] }; -} - -export function todoUrlAttachmentDeserializer(item: any): TodoUrlAttachment { - return { - description: item["description"], - url: item["url"], - }; -} - export function todoAttachmentArraySerializer( result: Array, ): any[] { @@ -225,19 +210,76 @@ export function todoAttachmentArrayDeserializer( }); } -/** Alias for TodoAttachment */ -export type TodoAttachment = TodoFileAttachment | TodoUrlAttachment; - -export function todoAttachmentSerializer(item: TodoAttachment): any { - return item; +/** model interface _CreateJsonResponse */ +export interface _CreateJsonResponse { + /** The item's unique id */ + readonly id: number; + /** The item's title */ + title: string; + /** User that created the todo */ + readonly createdBy: number; + /** User that the todo is assigned to */ + assignedTo?: number; + /** A longer description of the todo item in markdown format */ + description?: string; + /** The status of the todo item */ + status: "NotStarted" | "InProgress" | "Completed"; + /** When the todo item was created. */ + readonly createdAt: Date; + /** When the todo item was last updated */ + readonly updatedAt: Date; + /** When the todo item was makred as completed */ + readonly completedAt?: Date; + labels?: TodoLabels; } -export function todoAttachmentDeserializer(item: any): TodoAttachment { - return item; +export function _createJsonResponseDeserializer( + item: any, +): _CreateJsonResponse { + return { + id: item["id"], + title: item["title"], + createdBy: item["createdBy"], + assignedTo: item["assignedTo"], + description: item["description"], + status: item["status"], + createdAt: new Date(item["createdAt"]), + updatedAt: new Date(item["updatedAt"]), + completedAt: !item["completedAt"] + ? item["completedAt"] + : new Date(item["completedAt"]), + labels: !item["labels"] + ? item["labels"] + : todoLabelsDeserializer(item["labels"]), + }; } -/** model interface _CreateResponse */ -export interface _CreateResponse { +/** model interface ToDoItemMultipartRequest */ +export interface ToDoItemMultipartRequest { + item: TodoItem; + attachments?: Array< + | FileContents + | { contents: FileContents; contentType?: string; filename?: string } + >; +} + +export function toDoItemMultipartRequestSerializer( + item: ToDoItemMultipartRequest, +): any { + return [ + { name: "item", body: todoItemSerializer(item["item"]) }, + ...(item["attachments"] === undefined + ? [] + : [ + ...item["attachments"].map((x: unknown) => + createFilePartDescriptor("attachments", x), + ), + ]), + ]; +} + +/** model interface _CreateFormResponse */ +export interface _CreateFormResponse { /** The item's unique id */ readonly id: number; /** The item's title */ @@ -259,7 +301,9 @@ export interface _CreateResponse { labels?: TodoLabels; } -export function _createResponseDeserializer(item: any): _CreateResponse { +export function _createFormResponseDeserializer( + item: any, +): _CreateFormResponse { return { id: item["id"], title: item["title"], @@ -362,6 +406,19 @@ export function _updateResponseDeserializer(item: any): _UpdateResponse { }; } +/** model interface FileAttachmentMultipartRequest */ +export interface FileAttachmentMultipartRequest { + contents: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; +} + +export function fileAttachmentMultipartRequestSerializer( + item: FileAttachmentMultipartRequest, +): any { + return [createFilePartDescriptor("contents", item["contents"])]; +} + /** model interface User */ export interface User { /** An autogenerated unique id for the user */ @@ -385,8 +442,8 @@ export function userSerializer(item: User): any { }; } -/** model interface _CreateResponse1 */ -export interface _CreateResponse1 { +/** model interface _CreateResponse */ +export interface _CreateResponse { /** An autogenerated unique id for the user */ readonly id: number; /** The user's username */ @@ -397,7 +454,7 @@ export interface _CreateResponse1 { token: string; } -export function _createResponse1Deserializer(item: any): _CreateResponse1 { +export function _createResponseDeserializer(item: any): _CreateResponse { return { id: item["id"], username: item["username"], diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/todoItems/models.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/todoItems/models.ts index bc29a433bd..9a81d4ec27 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/todoItems/models.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/models/todoItems/models.ts @@ -4,8 +4,8 @@ import { todoItemArrayDeserializer, TodoItem, ApiError, - todoAttachmentArrayDeserializer, TodoAttachment, + todoAttachmentArrayDeserializer, } from "../models.js"; /** model interface TodoPage */ diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/static-helpers/multipartHelpers.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/static-helpers/multipartHelpers.ts new file mode 100644 index 0000000000..e704928cf9 --- /dev/null +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/static-helpers/multipartHelpers.ts @@ -0,0 +1,32 @@ +// Licensed under the MIT License. + +/** + * Valid values for the contents of a binary file. + */ +export type FileContents = + | string + | NodeJS.ReadableStream + | ReadableStream + | Uint8Array + | Blob; + +export function createFilePartDescriptor( + partName: string, + fileInput: any, + defaultContentType?: string, +): any { + if (fileInput.contents) { + return { + name: partName, + body: fileInput.contents, + contentType: fileInput.contentType ?? defaultContentType, + filename: fileInput.filename, + }; + } else { + return { + name: partName, + body: fileInput, + contentType: defaultContentType, + }; + } +} diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/todoClient.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/todoClient.ts index 3ea8460958..4429ec5aea 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/todoClient.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/todoClient.ts @@ -1,10 +1,10 @@ // Licensed under the MIT License. import { - getTodoItemsOperations, + _getTodoItemsOperations, TodoItemsOperations, } from "./classic/todoItems/index.js"; -import { getUsersOperations, UsersOperations } from "./classic/users/index.js"; +import { _getUsersOperations, UsersOperations } from "./classic/users/index.js"; import { createTodo, TodoContext, @@ -33,8 +33,8 @@ export class TodoClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.todoItems = getTodoItemsOperations(this._client); - this.users = getUsersOperations(this._client); + this.todoItems = _getTodoItemsOperations(this._client); + this.users = _getUsersOperations(this._client); } /** The operation groups for todoItems */ diff --git a/packages/typespec-test/test/todo_non_branded/spec/main.tsp b/packages/typespec-test/test/todo_non_branded/spec/main.tsp index 862caa6b7c..0507e08027 100644 --- a/packages/typespec-test/test/todo_non_branded/spec/main.tsp +++ b/packages/typespec-test/test/todo_non_branded/spec/main.tsp @@ -79,6 +79,15 @@ model TodoItem { @visibility("create") _dummy?: string; } +model ToDoItemMultipartRequest { + item: HttpPart; + attachments?: HttpPart[]; +} + +model FileAttachmentMultipartRequest { + contents: HttpPart; +} + @jsonSchema union TodoLabels { string, @@ -95,22 +104,8 @@ model TodoLabelRecord { color?: string; } -union TodoAttachment { - file: TodoFileAttachment, - url: TodoUrlAttachment, -} - -@jsonSchema -model TodoUrlAttachment { - /** A description of the URL */ - description: string; - - /** The url */ - url: url; -} - @jsonSchema -model TodoFileAttachment { +model TodoAttachment { /** The file name of the attachment */ @maxLength(255) filename: string; @@ -245,19 +240,28 @@ namespace TodoItems { code: "not-found"; } + //@friendlyName("{name}List", T) model Page { @pageItems items: T[]; } @list op list(...PaginationControls): WithStandardErrors; + @sharedRoute @post - op create( + op createJson( @header contentType: "application/json", item: TodoItem, attachments?: TodoAttachment[], ): WithStandardErrors; + @sharedRoute + @post + op createForm( + @header contentType: "multipart/form-data", + @multipartBody body: ToDoItemMultipartRequest, + ): WithStandardErrors; + @get op get(@path id: TodoItem.id): TodoItem | NotFoundErrorResponse; @patch op update( @header contentType: "application/merge-patch+json", @@ -276,9 +280,18 @@ namespace TodoItems { @sharedRoute @post - op createAttachment( + op createJsonAttachment( + @header contentType: "application/json", @path itemId: TodoItem.id, @body contents: TodoAttachment, ): WithStandardErrors; + + @sharedRoute + @post + op createFileAttachment( + @header contentType: "multipart/form-data", + @path itemId: TodoItem.id, + @multipartBody body: FileAttachmentMultipartRequest, + ): WithStandardErrors; } } diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/classic/budgets/index.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/classic/budgets/index.ts index da88cc679b..236fc9d346 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/classic/budgets/index.ts +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/classic/budgets/index.ts @@ -36,7 +36,7 @@ function _getBudgets(context: WidgetServiceContext) { }; } -export function getBudgetsOperations( +export function _getBudgetsOperations( context: WidgetServiceContext, ): BudgetsOperations { return { diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/classic/widgets/index.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/classic/widgets/index.ts index bf3c563b73..afc2667cf7 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/classic/widgets/index.ts +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/classic/widgets/index.ts @@ -145,7 +145,7 @@ function _getWidgets(context: WidgetServiceContext) { }; } -export function getWidgetsOperations( +export function _getWidgetsOperations( context: WidgetServiceContext, ): WidgetsOperations { return { diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/widgetServiceClient.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/widgetServiceClient.ts index d50863d698..3d3b32f265 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/widgetServiceClient.ts +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/widgetServiceClient.ts @@ -2,11 +2,11 @@ // Licensed under the MIT License. import { - getBudgetsOperations, + _getBudgetsOperations, BudgetsOperations, } from "./classic/budgets/index.js"; import { - getWidgetsOperations, + _getWidgetsOperations, WidgetsOperations, } from "./classic/widgets/index.js"; import { @@ -38,8 +38,8 @@ export class WidgetServiceClient { userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; - this.budgets = getBudgetsOperations(this._client); - this.widgets = getWidgetsOperations(this._client); + this.budgets = _getBudgetsOperations(this._client); + this.widgets = _getWidgetsOperations(this._client); } /** The operation groups for budgets */ diff --git a/packages/typespec-ts/src/index.ts b/packages/typespec-ts/src/index.ts index ec16c69891..5b3b855868 100644 --- a/packages/typespec-ts/src/index.ts +++ b/packages/typespec-ts/src/index.ts @@ -11,6 +11,7 @@ import { import { EmitContext, Program } from "@typespec/compiler"; import { GenerationDirDetail, SdkContext } from "./utils/interfaces.js"; import { + MultipartHelpers, PagingHelpers, PollingHelpers, SerializationHelpers @@ -118,7 +119,8 @@ export async function $onEmit(context: EmitContext) { { ...SerializationHelpers, ...PagingHelpers, - ...PollingHelpers + ...PollingHelpers, + ...MultipartHelpers }, { sourcesDir: dpgContext.generationPathDetail?.modularSourcesDir, diff --git a/packages/typespec-ts/src/modular/buildClassicalClient.ts b/packages/typespec-ts/src/modular/buildClassicalClient.ts index af58e0bbf2..a9e9714182 100644 --- a/packages/typespec-ts/src/modular/buildClassicalClient.ts +++ b/packages/typespec-ts/src/modular/buildClassicalClient.ts @@ -193,7 +193,7 @@ function buildClientOperationGroups( }); } else { const groupName = normalizeName(prefixes[0] ?? "", NameType.Property); - const operationName = `get${normalizeName( + const operationName = `_get${normalizeName( groupName, NameType.OperationGroup )}Operations`; @@ -222,7 +222,7 @@ function buildClientOperationGroups( clientClass .getConstructors()[0] ?.addStatements( - `this.${groupName} = get${normalizeName( + `this.${groupName} = _get${normalizeName( groupName, NameType.OperationGroup )}Operations(this._client)` diff --git a/packages/typespec-ts/src/modular/buildRootIndex.ts b/packages/typespec-ts/src/modular/buildRootIndex.ts index fa40967f56..b5c3e97f1a 100644 --- a/packages/typespec-ts/src/modular/buildRootIndex.ts +++ b/packages/typespec-ts/src/modular/buildRootIndex.ts @@ -3,7 +3,7 @@ import { Project, SourceFile } from "ts-morph"; import { getClassicalClientName } from "./helpers/namingHelpers.js"; import { ModularEmitterOptions } from "./interfaces.js"; import { resolveReference } from "../framework/reference.js"; -import { PagingHelpers } from "./static-helpers-metadata.js"; +import { MultipartHelpers, PagingHelpers } from "./static-helpers-metadata.js"; import { SdkClientType, SdkContext, @@ -70,6 +70,7 @@ export function buildRootIndex( }); exportPagingTypes(context, rootIndexFile); + exportFileContentsType(context, rootIndexFile); } /** @@ -108,6 +109,28 @@ function hasPaging(context: SdkContext): boolean { }); } +function exportFileContentsType( + context: SdkContext, + rootIndexFile: SourceFile +) { + if ( + context.sdkPackage.models.some((x) => + x.properties.some( + (y) => y.kind === "property" && y.multipartOptions?.isFilePart + ) + ) + ) { + const existingExports = getExistingExports(rootIndexFile); + const namedExports = [resolveReference(MultipartHelpers.FileContents)]; + + const newNamedExports = getNewNamedExports(namedExports, existingExports); + + if (newNamedExports.length > 0) { + addExportsToRootIndexFile(rootIndexFile, newNamedExports); + } + } +} + function getExistingExports(rootIndexFile: SourceFile): Set { return new Set( rootIndexFile diff --git a/packages/typespec-ts/src/modular/emitModels.ts b/packages/typespec-ts/src/modular/emitModels.ts index 668934b194..ab3d55a78d 100644 --- a/packages/typespec-ts/src/modular/emitModels.ts +++ b/packages/typespec-ts/src/modular/emitModels.ts @@ -45,7 +45,7 @@ import { import path from "path"; import { refkey } from "../framework/refkey.js"; import { useContext } from "../contextManager.js"; -import { isMetadata } from "@typespec/http"; +import { isMetadata, isOrExtendsHttpFile } from "@typespec/http"; import { isAzureCoreErrorType, isAzureCoreLroType @@ -55,6 +55,8 @@ import { isDiscriminatedUnion } from "./serialization/serializeUtils.js"; import { reportDiagnostic } from "../lib.js"; import { NoTarget } from "@typespec/compiler"; import { emitQueue } from "../framework/hooks/sdkTypes.js"; +import { resolveReference } from "../framework/reference.js"; +import { MultipartHelpers } from "./static-helpers-metadata.js"; type InterfaceStructure = OptionalKind & { extends?: string[]; @@ -126,6 +128,9 @@ function emitType(context: SdkContext, type: SdkType, sourceFile: SourceFile) { if (isAzureCoreErrorType(context.program, type.__raw)) { return; } + if (isOrExtendsHttpFile(context.program, type.__raw!)) { + return; + } if ( !type.usage || (type.usage !== undefined && @@ -566,10 +571,52 @@ function buildModelProperty( target: NoTarget }); } + + let typeExpression: string; + if (property.kind === "property" && property.isMultipartFileInput) { + const multipartOptions = property.multipartOptions; + typeExpression = "{"; + typeExpression += `contents: ${resolveReference(MultipartHelpers.FileContents)};`; + + const isContentTypeOptional = + multipartOptions?.contentType === undefined || + multipartOptions.contentType.optional || + multipartOptions.defaultContentTypes.length > 0; + const isFilenameOptional = + multipartOptions?.filename === undefined || + multipartOptions.filename.optional; + + const contentTypeType = multipartOptions?.contentType + ? getTypeExpression(context, multipartOptions.contentType.type) + : "string"; + const filenameType = multipartOptions?.filename + ? getTypeExpression(context, multipartOptions.filename.type) + : "string"; + + typeExpression += `contentType${isContentTypeOptional ? "?" : ""}: ${contentTypeType};`; + typeExpression += `filename${isFilenameOptional ? "?" : ""}: ${filenameType};`; + + typeExpression += "}"; + + if (isContentTypeOptional && isFilenameOptional) { + // Allow passing content directly if both filename and content type are optional + typeExpression = `(${resolveReference(MultipartHelpers.FileContents)}) | ${typeExpression}`; + } else { + // If either one is required, still accept File at the top level since it requires a filename + typeExpression = `File | ${typeExpression}`; + } + + if (property.type.kind === "array") { + typeExpression = `Array<${typeExpression}>`; + } + } else { + typeExpression = getTypeExpression(context, property.type); + } + const propertyStructure: PropertySignatureStructure = { kind: StructureKind.PropertySignature, name: normalizedPropName, - type: getTypeExpression(context, property.type), + type: typeExpression, hasQuestionToken: property.optional, isReadonly: isReadOnly(property as SdkBodyModelPropertyType) }; diff --git a/packages/typespec-ts/src/modular/helpers/classicalOperationHelpers.ts b/packages/typespec-ts/src/modular/helpers/classicalOperationHelpers.ts index 880cd34180..1816424b29 100644 --- a/packages/typespec-ts/src/modular/helpers/classicalOperationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/classicalOperationHelpers.ts @@ -74,21 +74,26 @@ export function getClassicalOperation( .filter((i) => i.getName() === interfaceName)[0]; const properties: OptionalKind[] = []; if (layer !== prefixes.length - 1) { - properties.push({ - kind: StructureKind.PropertySignature, - name: normalizeName( - (layer === prefixes.length - 1 - ? prefixes[layer] - : prefixes[layer + 1]) ?? "", - NameType.Property - ), - type: `${getClassicalLayerPrefix( - prefixes, - NameType.Interface, + const name = normalizeName( + (layer === prefixes.length - 1 ? prefixes[layer] : prefixes[layer + 1]) ?? "", - layer + 1 - )}Operations` - }); + NameType.Property + ); + if ( + !properties.some((x) => x.name === name) && + !(existInterface && existInterface.getProperty(name)) + ) { + properties.push({ + kind: StructureKind.PropertySignature, + name, + type: `${getClassicalLayerPrefix( + prefixes, + NameType.Interface, + "", + layer + 1 + )}Operations` + }); + } } else { operationDeclarations.forEach((d) => { properties.push({ @@ -164,7 +169,7 @@ export function getClassicalOperation( }); } - const operationFunctionName = `get${getClassicalLayerPrefix( + const operationFunctionName = `_get${getClassicalLayerPrefix( prefixes, NameType.Interface, "", @@ -176,27 +181,41 @@ export function getClassicalOperation( if (existFunction) { const returnStatement = existFunction.getBodyText(); if (returnStatement) { - let statement = `, + let statement: string | undefined = undefined; + if (layer !== prefixes.length - 1) { + const name = normalizeName( + prefixes[layer + 1] ?? "FIXME", + NameType.Property + ); + + // HACK: check if the statement includes a group of this name already to prevent an operation group appearing multiple times + // TODO: would be good to refactor so that we have an intermediate data structure before generating the return statement + if (!returnStatement.includes(`${name}:`)) { + statement = `, + ${normalizeName( + prefixes[layer + 1] ?? "FIXME", + NameType.Property + )}: _get${getClassicalLayerPrefix( + prefixes, + NameType.Interface, + "", + layer + 1 + )}Operations(context)}`; + } + } else { + statement = `, ..._get${getClassicalLayerPrefix( prefixes, NameType.Interface, "", layer + 1 )}Operations(context)}`; - if (layer !== prefixes.length - 1) { - statement = `, - ${normalizeName( - prefixes[layer + 1] ?? "FIXME", - NameType.Property - )}: get${getClassicalLayerPrefix( - prefixes, - NameType.Interface, - "", - layer + 1 - )}Operations(context)}`; } - const newReturnStatement = returnStatement.replace(/}$/, statement); - existFunction.setBodyText(newReturnStatement); + + if (statement) { + const newReturnStatement = returnStatement.replace(/}$/, statement); + existFunction.setBodyText(newReturnStatement); + } } } else { const functions = { @@ -220,7 +239,7 @@ export function getClassicalOperation( ${normalizeName( prefixes[layer + 1] ?? "FIXME", NameType.Property - )}: get${getClassicalLayerPrefix( + )}: _get${getClassicalLayerPrefix( prefixes, NameType.Interface, "", diff --git a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts index 6eb5774500..f5d214213a 100644 --- a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts @@ -892,7 +892,7 @@ function getRequired(context: SdkContext, param: SdkModelPropertyType) { const serializedName = getPropertySerializedName(param); const clientValue = `${param.onClient ? "context." : ""}${param.name}`; if (param.type.kind === "model") { - const { propertiesStr } = getRequestModelMapping( + const propertiesStr = getRequestModelMapping( context, { ...param.type, optional: param.optional }, clientValue @@ -931,15 +931,12 @@ function getOptional( const serializedName = getPropertySerializedName(param); const paramName = `${param.onClient ? "context." : `${optionalParamName}?.`}${param.name}`; if (param.type.kind === "model") { - const { propertiesStr, directAssignment } = getRequestModelMapping( + const propertiesStr = getRequestModelMapping( context, { ...param.type, optional: param.optional }, paramName + "?." ); - const serializeContent = - directAssignment === true - ? propertiesStr.join(",") - : `{${propertiesStr.join(",")}}`; + const serializeContent = `{${propertiesStr.join(",")}}`; return `"${serializedName}": ${serializeContent}`; } return `"${serializedName}": ${serializeRequestValue( @@ -1045,72 +1042,86 @@ function getNullableCheck(name: string, type: SdkType) { return `${name} === null ? null :`; } -/** - * - * This function helps translating an HLC request to RLC request, - * extracting properties from body and headers and building the RLC response object - */ -interface RequestModelMappingResult { - propertiesStr: string[]; - directAssignment?: boolean; +export function getSerializationExpression( + context: SdkContext, + property: SdkModelPropertyType, + propertyPath: string +): string { + const dot = propertyPath.endsWith("?") ? "." : ""; + const propertyPathWithDot = `${propertyPath ? `${propertyPath}${dot}` : `${dot}`}`; + const nullOrUndefinedPrefix = getPropertySerializationPrefix( + context, + property, + propertyPath + ); + + const propertyFullName = getPropertyFullName( + context, + property, + propertyPathWithDot + ); + const serializeFunctionName = buildModelSerializer( + context, + getNullableValidType(property.type), + false, + true + ); + if (serializeFunctionName) { + return `${nullOrUndefinedPrefix}${serializeFunctionName}(${propertyFullName})`; + } else if (isAzureCoreErrorType(context.program, property.type.__raw)) { + return `${nullOrUndefinedPrefix}${propertyFullName}`; + } else { + return serializeRequestValue( + context, + property.type, + propertyFullName, + !property.optional, + getEncodeForType(property.type) + ); + } } -export function getRequestModelMapping( + +export function getRequestModelProperties( context: SdkContext, modelPropertyType: SdkModelType & { optional?: boolean }, propertyPath: string = "body" -): RequestModelMappingResult { - const props: string[] = []; +): Array<[string, string]> { + const props: [string, string][] = []; const allParents = getAllAncestors(modelPropertyType); const properties: SdkModelPropertyType[] = getAllProperties(modelPropertyType, allParents) ?? []; if (properties.length <= 0) { - return { propertiesStr: [] }; + return []; } for (const property of properties) { if (property.kind === "property" && isReadOnly(property)) { continue; } - const dot = propertyPath.endsWith("?") ? "." : ""; - const serializedName = getPropertySerializedName(property); - const propertyPathWithDot = `${propertyPath ? `${propertyPath}${dot}` : `${dot}`}`; - const nullOrUndefinedPrefix = getPropertySerializationPrefix( - context, - property, - propertyPath - ); - const propertyFullName = getPropertyFullName( - context, - property, - propertyPathWithDot - ); - const serializeFunctionName = buildModelSerializer( - context, - getNullableValidType(property.type), - false, - true - ); - if (serializeFunctionName) { - props.push( - `"${serializedName}": ${nullOrUndefinedPrefix}${serializeFunctionName}(${propertyFullName})` - ); - } else if (isAzureCoreErrorType(context.program, property.type.__raw)) { - props.push( - `"${serializedName}": ${nullOrUndefinedPrefix}${propertyFullName}` - ); - } else { - const serializedValue = serializeRequestValue( - context, - property.type, - propertyFullName, - !property.optional, - getEncodeForType(property.type) - ); - props.push(`"${serializedName}": ${serializedValue}`); - } + props.push([ + getPropertySerializedName(property)!, + getSerializationExpression(context, property, propertyPath) + ]); } - return { propertiesStr: props }; + return props; +} + +/** + * + * This function helps translating an HLC request to RLC request, + * extracting properties from body and headers and building the RLC response object + */ +export function getRequestModelMapping( + context: SdkContext, + modelPropertyType: SdkModelType & { optional?: boolean }, + propertyPath: string = "body" +): string[] { + return getRequestModelProperties( + context, + modelPropertyType, + propertyPath + ).map(([name, value]) => `"${name}": ${value}`); } function getPropertySerializedName(property: SdkModelPropertyType) { @@ -1261,7 +1272,7 @@ export function serializeRequestValue( return `${clientValue} as any`; } case "model": // this is to build serialization logic for spread model types - return `{${getRequestModelMapping(context, type, "").propertiesStr.join(",")}}`; + return `{${getRequestModelMapping(context, type, "").join(",")}}`; case "nullable": return serializeRequestValue( context, diff --git a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts index 6d4f5a6dd8..ea4acfdbde 100644 --- a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts +++ b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts @@ -10,7 +10,13 @@ import { import { toCamelCase, toPascalCase } from "../../utils/casingUtils.js"; import { SdkContext } from "../../utils/interfaces.js"; -import { getRequestModelMapping } from "../helpers/operationHelpers.js"; +import { + getAllAncestors, + getAllProperties, + getPropertyFullName, + getRequestModelMapping, + getSerializationExpression +} from "../helpers/operationHelpers.js"; import { normalizeModelName } from "../emitModels.js"; import { NameType } from "@azure-tools/rlc-common"; import { isAzureCoreErrorType } from "../../utils/modelUtils.js"; @@ -19,6 +25,9 @@ import { isSupportedSerializeType, ModelSerializeOptions } from "./serializeUtils.js"; +import { MultipartHelpers } from "../static-helpers-metadata.js"; +import { resolveReference } from "../../framework/reference.js"; +import { isOrExtendsHttpFile } from "@typespec/http"; export function buildModelSerializer( context: SdkContext, @@ -43,7 +52,10 @@ export function buildModelSerializer( // throw new Error(`NYI Serialization of anonymous types`); return undefined; } - if (isAzureCoreErrorType(context.program, type.__raw!)) { + if ( + isAzureCoreErrorType(context.program, type.__raw!) || + isOrExtendsHttpFile(context.program, type.__raw!) + ) { return undefined; } } @@ -331,37 +343,90 @@ function buildModelTypeSerializer( statements: ["return item;"] }; - // This is only handling the compatibility mode, will need to update when we handle additionalProperties property. - const additionalPropertiesSpread = hasAdditionalProperties(type) - ? "...item" - : ""; + if ( + (type.usage & UsageFlags.Input) === UsageFlags.Input && + (type.usage & UsageFlags.MultipartFormData) === UsageFlags.MultipartFormData + ) { + // For MFD models, serialize into an array of parts + // TODO: cleaner abstraction, quite a bit of duplication with the non-MFD stuff here + const parts: string[] = []; - const { directAssignment, propertiesStr } = getRequestModelMapping( - context, - type, - "item" - ); - if (additionalPropertiesSpread) { - propertiesStr.unshift(additionalPropertiesSpread); - } - const serializeContent = - directAssignment === true - ? propertiesStr.join(",") - : `{${propertiesStr.join(",")}}`; + const properties = getAllProperties(type, getAllAncestors(type)); + for (const property of properties) { + if (property.kind !== "property") { + continue; + } + const expr = getSerializationExpression(context, property, "item"); + + let partDefinition: string; + if (property.isMultipartFileInput) { + const createFilePartDescriptorDefinition = resolveReference( + MultipartHelpers.createFilePartDescriptor + ); + + const itemPath = property.multipartOptions?.isMulti + ? "x" + : getPropertyFullName(context, property, "item"); + partDefinition = `${createFilePartDescriptorDefinition}("${property.serializedName}", ${itemPath}, )`; + + // If the TypeSpec doesn't specify a default content type, TCGC will infer a default of "*/*". + // In this case, we actually want the content type to be left unset so that Core will take care of + // setting the content type correctly. + const contentType = + property.multipartOptions?.defaultContentTypes?.[0] === "*/*" + ? undefined + : property.multipartOptions?.defaultContentTypes?.[0]; - const output = []; + if (property.multipartOptions?.isMulti) { + partDefinition = `...(item["${property.serializedName}"].map((x: unknown) => ${createFilePartDescriptorDefinition}("${property.serializedName}", x${contentType ? `", ${contentType}"` : ""})))`; + } else { + partDefinition = `${createFilePartDescriptorDefinition}("${property.serializedName}", item["${property.serializedName}"]${contentType ? `, "${contentType}"` : ""})`; + } + } else if (property.multipartOptions?.isMulti) { + partDefinition = `...((${expr}).map((x: unknown) => ({ name: "${property.serializedName}", body: x })))`; + } else { + partDefinition = `{ name: "${property.serializedName}", body: (${expr}) }`; + } + + if (property.optional) { + parts.push( + `...(${getPropertyFullName(context, property, "item")} === undefined ? [] : [${partDefinition}])` + ); + } else { + parts.push(partDefinition); + } + + // TODO: How to handle additionalProperties for MFD? + } + + serializerFunction.statements = [`return [${parts.join(",")}]`]; + } else { + // This is only handling the compatibility mode, will need to update when we handle additionalProperties property. + const additionalPropertiesSpread = hasAdditionalProperties(type) + ? "...item" + : ""; - // don't emit a serializer if there is nothing to serialize - if (propertiesStr.length) { - output.push(` + const propertiesStr = getRequestModelMapping(context, type, "item"); + + if (additionalPropertiesSpread) { + propertiesStr.unshift(additionalPropertiesSpread); + } + const serializeContent = `{${propertiesStr.join(",")}}`; + + const output = []; + + // don't emit a serializer if there is nothing to serialize + if (propertiesStr.length) { + output.push(` return ${serializeContent} `); - } else { - output.push(` + } else { + output.push(` return item; `); + } + serializerFunction.statements = output; } - serializerFunction.statements = output; return serializerFunction; } diff --git a/packages/typespec-ts/src/modular/static-helpers-metadata.ts b/packages/typespec-ts/src/modular/static-helpers-metadata.ts index 360cd35e80..90cf0904fe 100644 --- a/packages/typespec-ts/src/modular/static-helpers-metadata.ts +++ b/packages/typespec-ts/src/modular/static-helpers-metadata.ts @@ -71,3 +71,16 @@ export const PollingHelpers = { location: "pollingHelpers.ts" } } as const; + +export const MultipartHelpers = { + FileContents: { + kind: "typeAlias", + name: "FileContents", + location: "multipartHelpers.ts" + }, + createFilePartDescriptor: { + kind: "function", + name: "createFilePartDescriptor", + location: "multipartHelpers.ts" + } +} as const; diff --git a/packages/typespec-ts/static/static-helpers/multipartHelpers.ts b/packages/typespec-ts/static/static-helpers/multipartHelpers.ts new file mode 100644 index 0000000000..fb31e804dc --- /dev/null +++ b/packages/typespec-ts/static/static-helpers/multipartHelpers.ts @@ -0,0 +1,22 @@ + +/** + * Valid values for the contents of a binary file. + */ +export type FileContents = string | NodeJS.ReadableStream | ReadableStream | Uint8Array | Blob; + +export function createFilePartDescriptor(partName: string, fileInput: any, defaultContentType?: string): any { + if(fileInput.contents) { + return { + name: partName, + body: fileInput.contents, + contentType: fileInput.contentType ?? defaultContentType, + filename: fileInput.filename, + }; + } else { + return { + name: partName, + body: fileInput, + contentType: defaultContentType, + }; + } +} \ No newline at end of file diff --git a/packages/typespec-ts/test/azureModularIntegration/generated/payload/multipart/.gitignore b/packages/typespec-ts/test/azureModularIntegration/generated/payload/multipart/.gitignore new file mode 100644 index 0000000000..39220655cc --- /dev/null +++ b/packages/typespec-ts/test/azureModularIntegration/generated/payload/multipart/.gitignore @@ -0,0 +1,6 @@ +/** +!/src +/src/** +!/src/index.d.ts +!/.gitignore +!/tspconfig.yaml diff --git a/packages/typespec-ts/test/azureModularIntegration/generated/payload/multipart/src/index.d.ts b/packages/typespec-ts/test/azureModularIntegration/generated/payload/multipart/src/index.d.ts new file mode 100644 index 0000000000..3f90cc3d4c --- /dev/null +++ b/packages/typespec-ts/test/azureModularIntegration/generated/payload/multipart/src/index.d.ts @@ -0,0 +1,184 @@ +import { ClientOptions } from '@azure-rest/core-client'; +import { OperationOptions } from '@azure-rest/core-client'; +import { Pipeline } from '@azure/core-rest-pipeline'; + +export declare interface Address { + city: string; +} + +export declare interface BinaryArrayPartsRequest { + id: string; + pictures: Array; +} + +export declare interface ComplexHttpPartsModelRequest { + id: string; + address: Address; + profileImage: File | { + contents: FileContents; + contentType?: string; + filename: string; + }; + previousAddresses: Address[]; + pictures: Array; +} + +export declare interface ComplexPartsRequest { + id: string; + address: Address; + profileImage: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; + pictures: Array; +} + +export declare type FileContents = string | NodeJS.ReadableStream | ReadableStream | Uint8Array | Blob; + +export declare interface FileWithHttpPartOptionalContentTypeRequest { + profileImage: File | { + contents: FileContents; + contentType?: string; + filename: string; + }; +} + +export declare interface FileWithHttpPartRequiredContentTypeRequest { + profileImage: File | { + contents: FileContents; + contentType?: string; + filename: string; + }; +} + +export declare interface FileWithHttpPartSpecificContentTypeRequest { + profileImage: File | { + contents: FileContents; + contentType?: "image/jpg"; + filename: string; + }; +} + +export declare interface FormDataAnonymousModelOptionalParams extends OperationOptions { +} + +export declare interface FormDataBasicOptionalParams extends OperationOptions { +} + +export declare interface FormDataBinaryArrayPartsOptionalParams extends OperationOptions { +} + +export declare interface FormDataCheckFileNameAndContentTypeOptionalParams extends OperationOptions { +} + +export declare interface FormDataFileArrayAndBasicOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsContentTypeImageJpegContentTypeOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsContentTypeOperations { + optionalContentType: (body: FileWithHttpPartOptionalContentTypeRequest, options?: FormDataHttpPartsContentTypeOptionalContentTypeOptionalParams) => Promise; + requiredContentType: (body: FileWithHttpPartRequiredContentTypeRequest, options?: FormDataHttpPartsContentTypeRequiredContentTypeOptionalParams) => Promise; + imageJpegContentType: (body: FileWithHttpPartSpecificContentTypeRequest, options?: FormDataHttpPartsContentTypeImageJpegContentTypeOptionalParams) => Promise; +} + +export declare interface FormDataHttpPartsContentTypeOptionalContentTypeOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsContentTypeRequiredContentTypeOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsJsonArrayAndFileArrayOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsNonStringFloatOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsNonStringOperations { + float: (body: { + temperature: { + body: number; + contentType: "text/plain"; + }; + }, options?: FormDataHttpPartsNonStringFloatOptionalParams) => Promise; +} + +export declare interface FormDataHttpPartsOperations { + jsonArrayAndFileArray: (body: ComplexHttpPartsModelRequest, options?: FormDataHttpPartsJsonArrayAndFileArrayOptionalParams) => Promise; + nonString: FormDataHttpPartsNonStringOperations; + contentType: FormDataHttpPartsContentTypeOperations; +} + +export declare interface FormDataJsonPartOptionalParams extends OperationOptions { +} + +export declare interface FormDataMultiBinaryPartsOptionalParams extends OperationOptions { +} + +export declare interface FormDataOperations { + anonymousModel: (profileImage: Uint8Array, options?: FormDataAnonymousModelOptionalParams) => Promise; + checkFileNameAndContentType: (body: MultiPartRequest, options?: FormDataCheckFileNameAndContentTypeOptionalParams) => Promise; + multiBinaryParts: (body: MultiBinaryPartsRequest, options?: FormDataMultiBinaryPartsOptionalParams) => Promise; + binaryArrayParts: (body: BinaryArrayPartsRequest, options?: FormDataBinaryArrayPartsOptionalParams) => Promise; + jsonPart: (body: JsonPartRequest, options?: FormDataJsonPartOptionalParams) => Promise; + fileArrayAndBasic: (body: ComplexPartsRequest, options?: FormDataFileArrayAndBasicOptionalParams) => Promise; + basic: (body: MultiPartRequest, options?: FormDataBasicOptionalParams) => Promise; + httpParts: FormDataHttpPartsOperations; +} + +export declare interface JsonPartRequest { + address: Address; + profileImage: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; +} + +export declare interface MultiBinaryPartsRequest { + profileImage: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; + picture?: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; +} + +export declare class MultiPartClient { + private _client; + readonly pipeline: Pipeline; + constructor(options?: MultiPartClientOptionalParams); + readonly formData: FormDataOperations; +} + +export declare interface MultiPartClientOptionalParams extends ClientOptions { +} + +export declare interface MultiPartRequest { + id: string; + profileImage: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; +} + +export { } diff --git a/packages/typespec-ts/test/azureModularIntegration/generated/payload/multipart/tspconfig.yaml b/packages/typespec-ts/test/azureModularIntegration/generated/payload/multipart/tspconfig.yaml new file mode 100644 index 0000000000..68e490c843 --- /dev/null +++ b/packages/typespec-ts/test/azureModularIntegration/generated/payload/multipart/tspconfig.yaml @@ -0,0 +1,14 @@ +emit: + - "@azure-tools/typespec-ts" +options: + "@azure-tools/typespec-ts": + "emitter-output-dir": "{project-root}" + generateMetadata: true + generateTest: false + addCredentials: false + isTypeSpecTest: true + flavor: azure + isModularLibrary: true + packageDetails: + name: "@msinternal/payload-multipart" + description: "Payload Multipart Test Service" diff --git a/packages/typespec-ts/test/azureModularIntegration/payloadMultipart.spec.ts b/packages/typespec-ts/test/azureModularIntegration/payloadMultipart.spec.ts new file mode 100644 index 0000000000..e51a1803d6 --- /dev/null +++ b/packages/typespec-ts/test/azureModularIntegration/payloadMultipart.spec.ts @@ -0,0 +1,171 @@ +import { resolvePath } from "@typespec/compiler"; +import { MultiPartClient } from "./generated/payload/multipart/src/index.js"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import fs from "node:fs"; + +describe("Multipart Client", () => { + let client: MultiPartClient; + + const root = resolvePath(fileURLToPath(import.meta.url), "../../../temp"); + const imgPath = path.resolve(root, "./assets/image.jpg"); + const pngPath = path.resolve(root, "./assets/image.png"); + + beforeEach(() => { + client = new MultiPartClient({ + endpoint: "http://localhost:3002", + allowInsecureConnection: true + }); + }); + + it("basic multipart request", async () => { + await client.formData.basic({ + id: "123", + profileImage: { + contents: fs.createReadStream(imgPath), + // must specify a filename due to cadl-ranch limitations + filename: "test.jpg" + } + }); + }); + + // TODO not supported + it.skip("anonymous model", async () => { + // @ts-ignore - not supported yet + await client.formData.anonymousModel(fs.createReadStream(imgPath)); + }); + + it("binary array parts", async () => { + await client.formData.binaryArrayParts({ + id: "123", + pictures: [ + { filename: "test1.png", contents: fs.createReadStream(pngPath) }, + { filename: "test2.png", contents: fs.createReadStream(pngPath) } + ] + }); + }); + + it("filename and content type", async () => { + await client.formData.checkFileNameAndContentType({ + id: "123", + // This is the legacy non-httppart test which does not provide for default content type so we need to set it here + profileImage: { + filename: "hello.jpg", + contentType: "image/jpg", + contents: fs.createReadStream(imgPath) + } + }); + }); + + it("file array and basic", async () => { + await client.formData.fileArrayAndBasic({ + id: "123", + address: { + city: "X" + }, + profileImage: { + filename: "hello.jpg", + contents: fs.createReadStream(imgPath) + }, + pictures: [ + { filename: "test1.png", contents: fs.createReadStream(pngPath) }, + { filename: "test2.png", contents: fs.createReadStream(pngPath) } + ] + }); + }); + + it("json part", async () => { + await client.formData.jsonPart({ + address: { + city: "X" + }, + profileImage: { + filename: "hello.jpg", + contents: fs.createReadStream(imgPath) + } + }); + }); + + it("multi binary parts", async () => { + await client.formData.multiBinaryParts({ + profileImage: { + filename: "hello.jpg", + contents: fs.createReadStream(imgPath) + } + }); + + await client.formData.multiBinaryParts({ + profileImage: { + filename: "hello.jpg", + contents: fs.createReadStream(imgPath) + }, + picture: { + filename: "test1.png", + contents: fs.createReadStream(pngPath) + } + }); + }); + + describe("using HttpPart", () => { + it("JSON array and file array", async () => { + await client.formData.httpParts.jsonArrayAndFileArray({ + id: "123", + address: { + city: "X" + }, + profileImage: { + filename: "test.jpg", + contents: fs.createReadStream(imgPath) + }, + previousAddresses: [{ city: "Y" }, { city: "Z" }], + pictures: [ + { filename: "test1.png", contents: fs.createReadStream(pngPath) }, + { filename: "test2.png", contents: fs.createReadStream(pngPath) } + ] + }); + }); + + // TODO this isn't generating correctly + it.skip("non-string (float value)", async () => { + // @ts-expect-error - Model does not currently generate properly + await client.formData.httpParts.nonString.float({ temperature: 0.5 }); + }); + + describe("Content type", () => { + it("jpg image", async () => { + await client.formData.httpParts.contentType.imageJpegContentType({ + profileImage: { + contents: fs.createReadStream(imgPath), + filename: "hello.jpg" + } + }); + }); + + it("optional content type", async () => { + await client.formData.httpParts.contentType.optionalContentType({ + profileImage: { + contents: fs.createReadStream(imgPath), + filename: "hello.jpg" + } + }); + await client.formData.httpParts.contentType.optionalContentType({ + profileImage: { + contents: fs.createReadStream(imgPath), + filename: "hello.jpg", + contentType: "application/octet-stream" + } + }); + }); + + it("required content type", async () => { + await client.formData.httpParts.contentType.requiredContentType({ + profileImage: { + contents: fs.createReadStream(imgPath), + filename: "hello.jpg", + contentType: "application/octet-stream" + } + }); + }); + }); + }); +}); diff --git a/packages/typespec-ts/test/commands/cadl-ranch-list.js b/packages/typespec-ts/test/commands/cadl-ranch-list.js index e247761181..8a9e3b33f9 100644 --- a/packages/typespec-ts/test/commands/cadl-ranch-list.js +++ b/packages/typespec-ts/test/commands/cadl-ranch-list.js @@ -660,6 +660,10 @@ export const azureModularTsps = [ outputPath: "payload/media-type", inputPath: "payload/media-type" }, + { + outputPath: "payload/multipart", + inputPath: "payload/multipart" + }, { outputPath: "server/versions/versioned", inputPath: "server/versions/versioned" @@ -880,6 +884,10 @@ export const modularTsps = [ outputPath: "payload/media-type", inputPath: "payload/media-type" }, + { + outputPath: "payload/multipart", + inputPath: "payload/multipart" + }, { outputPath: "server/versions/versioned", inputPath: "server/versions/versioned" diff --git a/packages/typespec-ts/test/modularIntegration/generated/payload/multipart/.gitignore b/packages/typespec-ts/test/modularIntegration/generated/payload/multipart/.gitignore new file mode 100644 index 0000000000..39220655cc --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/generated/payload/multipart/.gitignore @@ -0,0 +1,6 @@ +/** +!/src +/src/** +!/src/index.d.ts +!/.gitignore +!/tspconfig.yaml diff --git a/packages/typespec-ts/test/modularIntegration/generated/payload/multipart/src/index.d.ts b/packages/typespec-ts/test/modularIntegration/generated/payload/multipart/src/index.d.ts new file mode 100644 index 0000000000..c9aee1aa3b --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/generated/payload/multipart/src/index.d.ts @@ -0,0 +1,184 @@ +import { ClientOptions } from '@typespec/ts-http-runtime'; +import { OperationOptions } from '@typespec/ts-http-runtime'; +import { Pipeline } from '@typespec/ts-http-runtime'; + +export declare interface Address { + city: string; +} + +export declare interface BinaryArrayPartsRequest { + id: string; + pictures: Array; +} + +export declare interface ComplexHttpPartsModelRequest { + id: string; + address: Address; + profileImage: File | { + contents: FileContents; + contentType?: string; + filename: string; + }; + previousAddresses: Address[]; + pictures: Array; +} + +export declare interface ComplexPartsRequest { + id: string; + address: Address; + profileImage: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; + pictures: Array; +} + +export declare type FileContents = string | NodeJS.ReadableStream | ReadableStream | Uint8Array | Blob; + +export declare interface FileWithHttpPartOptionalContentTypeRequest { + profileImage: File | { + contents: FileContents; + contentType?: string; + filename: string; + }; +} + +export declare interface FileWithHttpPartRequiredContentTypeRequest { + profileImage: File | { + contents: FileContents; + contentType?: string; + filename: string; + }; +} + +export declare interface FileWithHttpPartSpecificContentTypeRequest { + profileImage: File | { + contents: FileContents; + contentType?: "image/jpg"; + filename: string; + }; +} + +export declare interface FormDataAnonymousModelOptionalParams extends OperationOptions { +} + +export declare interface FormDataBasicOptionalParams extends OperationOptions { +} + +export declare interface FormDataBinaryArrayPartsOptionalParams extends OperationOptions { +} + +export declare interface FormDataCheckFileNameAndContentTypeOptionalParams extends OperationOptions { +} + +export declare interface FormDataFileArrayAndBasicOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsContentTypeImageJpegContentTypeOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsContentTypeOperations { + optionalContentType: (body: FileWithHttpPartOptionalContentTypeRequest, options?: FormDataHttpPartsContentTypeOptionalContentTypeOptionalParams) => Promise; + requiredContentType: (body: FileWithHttpPartRequiredContentTypeRequest, options?: FormDataHttpPartsContentTypeRequiredContentTypeOptionalParams) => Promise; + imageJpegContentType: (body: FileWithHttpPartSpecificContentTypeRequest, options?: FormDataHttpPartsContentTypeImageJpegContentTypeOptionalParams) => Promise; +} + +export declare interface FormDataHttpPartsContentTypeOptionalContentTypeOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsContentTypeRequiredContentTypeOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsJsonArrayAndFileArrayOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsNonStringFloatOptionalParams extends OperationOptions { +} + +export declare interface FormDataHttpPartsNonStringOperations { + float: (body: { + temperature: { + body: number; + contentType: "text/plain"; + }; + }, options?: FormDataHttpPartsNonStringFloatOptionalParams) => Promise; +} + +export declare interface FormDataHttpPartsOperations { + jsonArrayAndFileArray: (body: ComplexHttpPartsModelRequest, options?: FormDataHttpPartsJsonArrayAndFileArrayOptionalParams) => Promise; + nonString: FormDataHttpPartsNonStringOperations; + contentType: FormDataHttpPartsContentTypeOperations; +} + +export declare interface FormDataJsonPartOptionalParams extends OperationOptions { +} + +export declare interface FormDataMultiBinaryPartsOptionalParams extends OperationOptions { +} + +export declare interface FormDataOperations { + anonymousModel: (profileImage: Uint8Array, options?: FormDataAnonymousModelOptionalParams) => Promise; + checkFileNameAndContentType: (body: MultiPartRequest, options?: FormDataCheckFileNameAndContentTypeOptionalParams) => Promise; + multiBinaryParts: (body: MultiBinaryPartsRequest, options?: FormDataMultiBinaryPartsOptionalParams) => Promise; + binaryArrayParts: (body: BinaryArrayPartsRequest, options?: FormDataBinaryArrayPartsOptionalParams) => Promise; + jsonPart: (body: JsonPartRequest, options?: FormDataJsonPartOptionalParams) => Promise; + fileArrayAndBasic: (body: ComplexPartsRequest, options?: FormDataFileArrayAndBasicOptionalParams) => Promise; + basic: (body: MultiPartRequest, options?: FormDataBasicOptionalParams) => Promise; + httpParts: FormDataHttpPartsOperations; +} + +export declare interface JsonPartRequest { + address: Address; + profileImage: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; +} + +export declare interface MultiBinaryPartsRequest { + profileImage: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; + picture?: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; +} + +export declare class MultiPartClient { + private _client; + readonly pipeline: Pipeline; + constructor(options?: MultiPartClientOptionalParams); + readonly formData: FormDataOperations; +} + +export declare interface MultiPartClientOptionalParams extends ClientOptions { +} + +export declare interface MultiPartRequest { + id: string; + profileImage: FileContents | { + contents: FileContents; + contentType?: string; + filename?: string; + }; +} + +export { } diff --git a/packages/typespec-ts/test/modularIntegration/generated/payload/multipart/tspconfig.yaml b/packages/typespec-ts/test/modularIntegration/generated/payload/multipart/tspconfig.yaml new file mode 100644 index 0000000000..d1f333d409 --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/generated/payload/multipart/tspconfig.yaml @@ -0,0 +1,12 @@ +emit: + - "@azure-tools/typespec-ts" +options: + "@azure-tools/typespec-ts": + "emitter-output-dir": "{project-root}" + generateMetadata: true + generateTest: false + addCredentials: false + isTypeSpecTest: true + packageDetails: + name: "@unbranded/payload-multipart" + description: "Payload Multipart Test Service" diff --git a/packages/typespec-ts/test/modularIntegration/payloadMultipart.spec.ts b/packages/typespec-ts/test/modularIntegration/payloadMultipart.spec.ts new file mode 100644 index 0000000000..e51a1803d6 --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/payloadMultipart.spec.ts @@ -0,0 +1,171 @@ +import { resolvePath } from "@typespec/compiler"; +import { MultiPartClient } from "./generated/payload/multipart/src/index.js"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import fs from "node:fs"; + +describe("Multipart Client", () => { + let client: MultiPartClient; + + const root = resolvePath(fileURLToPath(import.meta.url), "../../../temp"); + const imgPath = path.resolve(root, "./assets/image.jpg"); + const pngPath = path.resolve(root, "./assets/image.png"); + + beforeEach(() => { + client = new MultiPartClient({ + endpoint: "http://localhost:3002", + allowInsecureConnection: true + }); + }); + + it("basic multipart request", async () => { + await client.formData.basic({ + id: "123", + profileImage: { + contents: fs.createReadStream(imgPath), + // must specify a filename due to cadl-ranch limitations + filename: "test.jpg" + } + }); + }); + + // TODO not supported + it.skip("anonymous model", async () => { + // @ts-ignore - not supported yet + await client.formData.anonymousModel(fs.createReadStream(imgPath)); + }); + + it("binary array parts", async () => { + await client.formData.binaryArrayParts({ + id: "123", + pictures: [ + { filename: "test1.png", contents: fs.createReadStream(pngPath) }, + { filename: "test2.png", contents: fs.createReadStream(pngPath) } + ] + }); + }); + + it("filename and content type", async () => { + await client.formData.checkFileNameAndContentType({ + id: "123", + // This is the legacy non-httppart test which does not provide for default content type so we need to set it here + profileImage: { + filename: "hello.jpg", + contentType: "image/jpg", + contents: fs.createReadStream(imgPath) + } + }); + }); + + it("file array and basic", async () => { + await client.formData.fileArrayAndBasic({ + id: "123", + address: { + city: "X" + }, + profileImage: { + filename: "hello.jpg", + contents: fs.createReadStream(imgPath) + }, + pictures: [ + { filename: "test1.png", contents: fs.createReadStream(pngPath) }, + { filename: "test2.png", contents: fs.createReadStream(pngPath) } + ] + }); + }); + + it("json part", async () => { + await client.formData.jsonPart({ + address: { + city: "X" + }, + profileImage: { + filename: "hello.jpg", + contents: fs.createReadStream(imgPath) + } + }); + }); + + it("multi binary parts", async () => { + await client.formData.multiBinaryParts({ + profileImage: { + filename: "hello.jpg", + contents: fs.createReadStream(imgPath) + } + }); + + await client.formData.multiBinaryParts({ + profileImage: { + filename: "hello.jpg", + contents: fs.createReadStream(imgPath) + }, + picture: { + filename: "test1.png", + contents: fs.createReadStream(pngPath) + } + }); + }); + + describe("using HttpPart", () => { + it("JSON array and file array", async () => { + await client.formData.httpParts.jsonArrayAndFileArray({ + id: "123", + address: { + city: "X" + }, + profileImage: { + filename: "test.jpg", + contents: fs.createReadStream(imgPath) + }, + previousAddresses: [{ city: "Y" }, { city: "Z" }], + pictures: [ + { filename: "test1.png", contents: fs.createReadStream(pngPath) }, + { filename: "test2.png", contents: fs.createReadStream(pngPath) } + ] + }); + }); + + // TODO this isn't generating correctly + it.skip("non-string (float value)", async () => { + // @ts-expect-error - Model does not currently generate properly + await client.formData.httpParts.nonString.float({ temperature: 0.5 }); + }); + + describe("Content type", () => { + it("jpg image", async () => { + await client.formData.httpParts.contentType.imageJpegContentType({ + profileImage: { + contents: fs.createReadStream(imgPath), + filename: "hello.jpg" + } + }); + }); + + it("optional content type", async () => { + await client.formData.httpParts.contentType.optionalContentType({ + profileImage: { + contents: fs.createReadStream(imgPath), + filename: "hello.jpg" + } + }); + await client.formData.httpParts.contentType.optionalContentType({ + profileImage: { + contents: fs.createReadStream(imgPath), + filename: "hello.jpg", + contentType: "application/octet-stream" + } + }); + }); + + it("required content type", async () => { + await client.formData.httpParts.contentType.requiredContentType({ + profileImage: { + contents: fs.createReadStream(imgPath), + filename: "hello.jpg", + contentType: "application/octet-stream" + } + }); + }); + }); + }); +}); diff --git a/packages/typespec-ts/test/modularUnit/scenarios/apiOperations/apiOperations.md b/packages/typespec-ts/test/modularUnit/scenarios/apiOperations/apiOperations.md index fdaf91c0d9..6d660836d5 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/apiOperations/apiOperations.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/apiOperations/apiOperations.md @@ -140,7 +140,9 @@ export async function uploadFileViaBody( /** model interface _UploadFileRequest */ export interface _UploadFileRequest { name: string; - file: Uint8Array; + file: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; } ``` @@ -148,10 +150,10 @@ export interface _UploadFileRequest { ```ts models function _uploadFileRequestSerializer export function _uploadFileRequestSerializer(item: _UploadFileRequest): any { - return { - name: item["name"], - file: uint8ArrayToString(item["file"], "base64"), - }; + return [ + { name: "name", body: item["name"] }, + createFilePartDescriptor("file", item["file"]), + ]; } ``` @@ -227,19 +229,23 @@ scalar BinaryBytes extends bytes; ## Models ```ts models -import { uint8ArrayToString } from "@azure/core-util"; +import { + FileContents, + createFilePartDescriptor, +} from "../static-helpers/multipartHelpers.js"; /** model interface _UploadFilesRequest */ export interface _UploadFilesRequest { - files: Uint8Array[]; + files: Array< + | FileContents + | { contents: FileContents; contentType?: string; filename?: string } + >; } export function _uploadFilesRequestSerializer(item: _UploadFilesRequest): any { - return { - files: item["files"].map((p: any) => { - return uint8ArrayToString(p, "base64"); - }), - }; + return [ + ...item["files"].map((x: unknown) => createFilePartDescriptor("files", x)), + ]; } ``` diff --git a/packages/typespec-ts/test/modularUnit/scenarios/multipart/file.md b/packages/typespec-ts/test/modularUnit/scenarios/multipart/file.md new file mode 100644 index 0000000000..e5601fe09f --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/scenarios/multipart/file.md @@ -0,0 +1,183 @@ +# Basic file part + +```tsp +model RequestBody { + basicFile: HttpPart; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +This basic case uses TypeSpec's `Http.File`, which specifies an optional `filename` and `contentType`. Since both are optional, the customer can pass the file's content directly to the `basicFile` property. If the customer wants to specify the filename or content type, they can use the wrapper object. + +```ts models +import { + FileContents, + createFilePartDescriptor, +} from "../static-helpers/multipartHelpers.js"; + +/** model interface RequestBody */ +export interface RequestBody { + basicFile: + | FileContents + | { contents: FileContents; contentType?: string; filename?: string }; +} + +export function requestBodySerializer(item: RequestBody): any { + return [createFilePartDescriptor("basicFile", item["basicFile"])]; +} +``` + +## Operations + +```ts operations +import { TestingContext as Client } from "./index.js"; +import { RequestBody, requestBodySerializer } from "../models/models.js"; +import { + StreamableMethod, + PathUncheckedResponse, + createRestError, + operationOptionsToRequestParameters, +} from "@azure-rest/core-client"; + +export function _doThingSend( + context: Client, + bodyParam: RequestBody, + options: DoThingOptionalParams = { requestOptions: {} }, +): StreamableMethod { + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: "multipart/form-data", + body: requestBodySerializer(bodyParam), + }); +} + +export async function _doThingDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["204"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return; +} + +export async function doThing( + context: Client, + bodyParam: RequestBody, + options: DoThingOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _doThingSend(context, bodyParam, options); + return _doThingDeserialize(result); +} +``` + +# File part, filename required + +```tsp +model FileRequiredName extends File { + filename: string; +} + +model RequestBody { + nameRequired: HttpPart; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +The filename must be provided _somehow_. This can either be done by passing a `File` object, which has a required filename property, or by using the wrapper object to pass a `filename` alongside the `contents`. + +```ts models +import { + FileContents, + createFilePartDescriptor, +} from "../static-helpers/multipartHelpers.js"; + +/** model interface RequestBody */ +export interface RequestBody { + nameRequired: + | File + | { contents: FileContents; contentType?: string; filename: string }; +} + +export function requestBodySerializer(item: RequestBody): any { + return [createFilePartDescriptor("nameRequired", item["nameRequired"])]; +} +``` + +# Default content type + +```tsp +model PngFile extends File { + contentType: "image/png"; +} + +model RequestBody { + image: HttpPart; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +```ts models +import { + FileContents, + createFilePartDescriptor, +} from "../static-helpers/multipartHelpers.js"; + +/** model interface RequestBody */ +export interface RequestBody { + image: + | FileContents + | { contents: FileContents; contentType?: "image/png"; filename?: string }; +} + +export function requestBodySerializer(item: RequestBody): any { + return [createFilePartDescriptor("image", item["image"], "image/png")]; +} +``` + +# Multiple files + +```tsp +model RequestBody { + files: HttpPart[]; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +Each provided file in the input corresponds to one part in the multipart request. + +```ts models +import { + FileContents, + createFilePartDescriptor, +} from "../static-helpers/multipartHelpers.js"; + +/** model interface RequestBody */ +export interface RequestBody { + files: Array< + | FileContents + | { contents: FileContents; contentType?: string; filename?: string } + >; +} + +export function requestBodySerializer(item: RequestBody): any { + return [ + ...item["files"].map((x: unknown) => createFilePartDescriptor("files", x)), + ]; +} +``` \ No newline at end of file diff --git a/packages/typespec-ts/test/modularUnit/scenarios/multipart/json.md b/packages/typespec-ts/test/modularUnit/scenarios/multipart/json.md new file mode 100644 index 0000000000..5c678eb3b3 --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/scenarios/multipart/json.md @@ -0,0 +1,161 @@ +# Basic JSON part + +```tsp +model Person { + firstName: string; + lastName: string; + @encode("unixTimestamp", int32) + dateOfBirth: utcDateTime; +} + +model RequestBody { + person: HttpPart; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +The part should get generated correctly. The generated serializer should be used so that the date of birth is encoded correctly. + +```ts models +/** model interface RequestBody */ +export interface RequestBody { + person: Person; +} + +export function requestBodySerializer(item: RequestBody): any { + return [{ name: "person", body: personSerializer(item["person"]) }]; +} + +/** model interface Person */ +export interface Person { + firstName: string; + lastName: string; + dateOfBirth: Date; +} + +export function personSerializer(item: Person): any { + return { + firstName: item["firstName"], + lastName: item["lastName"], + dateOfBirth: (item["dateOfBirth"].getTime() / 1000) | 0, + }; +} +``` + +# JSON array + +This TypeSpec represents a multipart request body with one part. That part consists of a JSON array. This contrasts with the one-to-many JSON part case. + +```tsp +model Person { + firstName: string; + lastName: string; + @encode("unixTimestamp", int32) + dateOfBirth: utcDateTime; +} + +model RequestBody { + people: HttpPart; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +In this case one part is constructed from the serialized array. + +```ts models +/** model interface RequestBody */ +export interface RequestBody { + people: Person[]; +} + +export function requestBodySerializer(item: RequestBody): any { + return [{ name: "people", body: personArraySerializer(item["people"]) }]; +} + +export function personArraySerializer(result: Array): any[] { + return result.map((item) => { + return personSerializer(item); + }); +} + +/** model interface Person */ +export interface Person { + firstName: string; + lastName: string; + dateOfBirth: Date; +} + +export function personSerializer(item: Person): any { + return { + firstName: item["firstName"], + lastName: item["lastName"], + dateOfBirth: (item["dateOfBirth"].getTime() / 1000) | 0, + }; +} +``` + +# Array of parts + +This TypeSpec represents a multipart request with multiple JSON parts, each following the spec for `Person`. + +```tsp +model Person { + firstName: string; + lastName: string; + @encode("unixTimestamp", int32) + dateOfBirth: utcDateTime; +} + +model RequestBody { + people: HttpPart[]; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +In this case each element in the serialized array is converted to a part descriptor. + +```ts models +/** model interface RequestBody */ +export interface RequestBody { + people: Person[]; +} + +export function requestBodySerializer(item: RequestBody): any { + return [ + ...personArraySerializer(item["people"]).map((x: unknown) => ({ + name: "people", + body: x, + })), + ]; +} + +export function personArraySerializer(result: Array): any[] { + return result.map((item) => { + return personSerializer(item); + }); +} + +/** model interface Person */ +export interface Person { + firstName: string; + lastName: string; + dateOfBirth: Date; +} + +export function personSerializer(item: Person): any { + return { + firstName: item["firstName"], + lastName: item["lastName"], + dateOfBirth: (item["dateOfBirth"].getTime() / 1000) | 0, + }; +} +``` \ No newline at end of file diff --git a/packages/typespec-ts/test/modularUnit/scenarios/multipart/text.md b/packages/typespec-ts/test/modularUnit/scenarios/multipart/text.md new file mode 100644 index 0000000000..58a2f05c76 --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/scenarios/multipart/text.md @@ -0,0 +1,136 @@ +# Text parts + +## TypeSpec + +```tsp +model RequestBody { + firstName: HttpPart; + lastName: HttpPart; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +```ts models +/** model interface RequestBody */ +export interface RequestBody { + firstName: string; + lastName: string; +} + +export function requestBodySerializer(item: RequestBody): any { + return [ + { name: "firstName", body: item["firstName"] }, + { name: "lastName", body: item["lastName"] }, + ]; +} +``` + +## Operations + +```ts operations +import { TestingContext as Client } from "./index.js"; +import { RequestBody, requestBodySerializer } from "../models/models.js"; +import { + StreamableMethod, + PathUncheckedResponse, + createRestError, + operationOptionsToRequestParameters, +} from "@azure-rest/core-client"; + +export function _doThingSend( + context: Client, + bodyParam: RequestBody, + options: DoThingOptionalParams = { requestOptions: {} }, +): StreamableMethod { + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: "multipart/form-data", + body: requestBodySerializer(bodyParam), + }); +} + +export async function _doThingDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["204"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return; +} + +export async function doThing( + context: Client, + bodyParam: RequestBody, + options: DoThingOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _doThingSend(context, bodyParam, options); + return _doThingDeserialize(result); +} +``` + +# Optionality + +```tsp +model RequestBody { + lastName?: HttpPart; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +If a part is optional, not specifying a value should cause no part to be sent in the request. + +```ts models +/** model interface RequestBody */ +export interface RequestBody { + lastName?: string; +} + +export function requestBodySerializer(item: RequestBody): any { + return [ + ...(item["lastName"] === undefined + ? [] + : [{ name: "lastName", body: item["lastName"] }]), + ]; +} +``` + +# Array of text parts + +This case is multiple text parts + +```tsp +model RequestBody { + names: HttpPart[]; +} + +op doThing(@header contentType: "multipart/form-data", @multipartBody bodyParam: RequestBody): void; +``` + +## Models + +```ts models +/** model interface RequestBody */ +export interface RequestBody { + names: string[]; +} + +export function requestBodySerializer(item: RequestBody): any { + return [ + ...item["names"] + .map((p: any) => { + return p; + }) + .map((x: unknown) => ({ name: "names", body: x })), + ]; +} +``` \ No newline at end of file diff --git a/packages/typespec-ts/test/util/testUtil.ts b/packages/typespec-ts/test/util/testUtil.ts index ebb4e02888..5a48de6712 100644 --- a/packages/typespec-ts/test/util/testUtil.ts +++ b/packages/typespec-ts/test/util/testUtil.ts @@ -22,6 +22,7 @@ import { loadStaticHelpers } from "../../src/framework/load-static-helpers.js"; import path from "path"; import { getDirname } from "../../src/utils/dirname.js"; import { + MultipartHelpers, PagingHelpers, PollingHelpers, SerializationHelpers @@ -263,7 +264,8 @@ export async function provideBinderWithAzureDependencies(project: Project) { const staticHelpers = { ...SerializationHelpers, ...PagingHelpers, - ...PollingHelpers + ...PollingHelpers, + ...MultipartHelpers, }; const staticHelperMap = await loadStaticHelpers(project, staticHelpers, {