diff --git a/.chronus/changes/add-multipartfile-model-2024-8-2-16-33-33.md b/.chronus/changes/add-multipartfile-model-2024-8-2-16-33-33.md new file mode 100644 index 0000000000..45496564ec --- /dev/null +++ b/.chronus/changes/add-multipartfile-model-2024-8-2-16-33-33.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-azure-core" +--- + +Add model MultiPartFile with required `filename` and `contentType` \ No newline at end of file diff --git a/docs/libraries/azure-core/reference/data-types.md b/docs/libraries/azure-core/reference/data-types.md index 12d9ca1719..2241742137 100644 --- a/docs/libraries/azure-core/reference/data-types.md +++ b/docs/libraries/azure-core/reference/data-types.md @@ -178,6 +178,21 @@ model Azure.Core.ExpandQueryParameter | ------- | ---------- | ------------------------------------------------- | | expand? | `string[]` | Expand the indicated resources into the response. | +### `FileWithRequiredMetadata` {#Azure.Core.FileWithRequiredMetadata} + +Used in file part of multipart request body + +```typespec +model Azure.Core.FileWithRequiredMetadata +``` + +#### Properties + +| Name | Type | Description | +| ----------- | -------- | ------------------------------------------------------- | +| filename | `string` | The file name in file part of multipart request body | +| contentType | `string` | The content type in file part of multipart request body | + ### `FilterParameter` {#Azure.Core.FilterParameter} Provides the standard 'filter' query parameter for list operations diff --git a/docs/libraries/azure-core/reference/index.mdx b/docs/libraries/azure-core/reference/index.mdx index 5ea11012a4..4e29e82c1a 100644 --- a/docs/libraries/azure-core/reference/index.mdx +++ b/docs/libraries/azure-core/reference/index.mdx @@ -93,6 +93,7 @@ npm install --save-peer @azure-tools/typespec-azure-core - [`EtagProperty`](./data-types.md#Azure.Core.EtagProperty) - [`EtagResponseEnvelope`](./data-types.md#Azure.Core.EtagResponseEnvelope) - [`ExpandQueryParameter`](./data-types.md#Azure.Core.ExpandQueryParameter) +- [`FileWithRequiredMetadata`](./data-types.md#Azure.Core.FileWithRequiredMetadata) - [`FilterParameter`](./data-types.md#Azure.Core.FilterParameter) - [`FilterQueryParameter`](./data-types.md#Azure.Core.FilterQueryParameter) - [`MaxPageSizeQueryParameter`](./data-types.md#Azure.Core.MaxPageSizeQueryParameter) diff --git a/packages/typespec-azure-core/lib/models.tsp b/packages/typespec-azure-core/lib/models.tsp index 308bf98d00..a1ec834f8c 100644 --- a/packages/typespec-azure-core/lib/models.tsp +++ b/packages/typespec-azure-core/lib/models.tsp @@ -400,3 +400,12 @@ model ArmResourceIdentifierAllowedResource { */ scopes?: ArmResourceDeploymentScope[]; } + +@doc("Used in file part of multipart request body") +model FileWithRequiredMetadata extends File { + /** The file name in file part of multipart request body */ + filename: string; + + /** The content type in file part of multipart request body */ + contentType: string; +} diff --git a/packages/typespec-azure-core/test/types/models.test.ts b/packages/typespec-azure-core/test/types/models.test.ts new file mode 100644 index 0000000000..68cbc66972 --- /dev/null +++ b/packages/typespec-azure-core/test/types/models.test.ts @@ -0,0 +1,26 @@ +import { getHttpPart, isOrExtendsHttpFile } from "@typespec/http"; +import { ok, strictEqual } from "assert"; +import { it } from "vitest"; +import { getOperations } from "../test-host.js"; + +it("FileWithRequiredMetadata could be recognized by @typespec/http", async () => { + const [operations, _, runner] = await getOperations( + ` + model TestModel { + file: HttpPart; + }; + + @post op TestOperation(@header contentType: "multipart/form-date", @multipartBody body: TestModel): void; + ` + ); + + ok(operations.length === 1); + ok(operations[0].parameters.body); + strictEqual(operations[0].parameters.body.bodyKind, "multipart"); + strictEqual(operations[0].parameters.body.type.kind, "Model"); + const fileHttpPart = operations[0].parameters.body.type.properties.get("file"); + ok(fileHttpPart); + const file = getHttpPart(runner.program, fileHttpPart.type); + ok(file !== undefined); + ok(isOrExtendsHttpFile(runner.program, file.type)); +}); diff --git a/packages/typespec-client-generator-core/test/types/multipart-types.test.ts b/packages/typespec-client-generator-core/test/types/multipart-types.test.ts index e85ab84033..908a130d83 100644 --- a/packages/typespec-client-generator-core/test/types/multipart-types.test.ts +++ b/packages/typespec-client-generator-core/test/types/multipart-types.test.ts @@ -1,3 +1,4 @@ +import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing"; import { expectDiagnostics } from "@typespec/compiler/testing"; import { deepEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; @@ -454,6 +455,52 @@ describe("typespec-client-generator-core: multipart types", () => { strictEqual(fileRequiredFileName.multipartOptions.contentType.optional, false); }); + it("with FileWithRequiredMetadata of Azure.Core", async function () { + const runnerCore = await createSdkTestRunner({ + librariesToAdd: [AzureCoreTestLibrary], + emitterName: "@azure-tools/typespec-java", + autoUsings: ["Azure.Core"], + }); + await runnerCore.compileWithBuiltInService(` + model MultiPartRequest{ + fileOptionalFileName: HttpPart; + fileRequiredFileName: HttpPart; + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runnerCore.context.sdkPackage.models; + strictEqual(models.length, 2); + const MultiPartRequest = models.find((x) => x.name === "MultiPartRequest"); + ok(MultiPartRequest); + ok(MultiPartRequest.usage & UsageFlags.MultipartFormData); + const fileOptionalFileName = MultiPartRequest.properties.find( + (x) => x.name === "fileOptionalFileName" + ) as SdkBodyModelPropertyType; + ok(fileOptionalFileName); + strictEqual(fileOptionalFileName.optional, false); + ok(fileOptionalFileName.multipartOptions); + strictEqual(fileOptionalFileName.name, "fileOptionalFileName"); + strictEqual(fileOptionalFileName.multipartOptions.isFilePart, true); + ok(fileOptionalFileName.multipartOptions.filename); + strictEqual(fileOptionalFileName.multipartOptions.filename.optional, true); + ok(fileOptionalFileName.multipartOptions.contentType); + strictEqual(fileOptionalFileName.multipartOptions.contentType.optional, true); + + const fileRequiredFileName = MultiPartRequest.properties.find( + (x) => x.name === "fileRequiredFileName" + ) as SdkBodyModelPropertyType; + ok(fileRequiredFileName); + strictEqual(fileRequiredFileName.optional, false); + ok(fileRequiredFileName.multipartOptions); + strictEqual(fileRequiredFileName.name, "fileRequiredFileName"); + strictEqual(fileRequiredFileName.multipartOptions.isFilePart, true); + ok(fileRequiredFileName.multipartOptions.filename); + strictEqual(fileRequiredFileName.multipartOptions.filename.optional, false); + ok(fileRequiredFileName.multipartOptions.contentType); + strictEqual(fileRequiredFileName.multipartOptions.contentType.optional, false); + }); + it("check 'multi' of multipart with @multipartBody for model", async function () { await runner.compileWithBuiltInService(` model Address {