From b364a0efcadaa16e604fc0cc31a3898c755db6eb Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 22 May 2024 08:54:53 -0700 Subject: [PATCH 01/17] Add multipart part support --- core | 2 +- packages/typespec-autorest/src/openapi.ts | 156 +++++++++++++----- .../src/openapi2-document.ts | 14 +- .../typespec-autorest/test/multipart.test.ts | 100 ++++++++++- .../src/rules/arm-post-response-codes.ts | 5 +- .../src/http.ts | 2 +- .../src/package.ts | 6 +- 7 files changed, 239 insertions(+), 46 deletions(-) diff --git a/core b/core index 71784e2bc2..a63a6f616c 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 71784e2bc29e1c0ae937d6ff93258f9129e8606a +Subproject commit a63a6f616c3a90e28f6ccb8ffddcb6642962b000 diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index f366ec95e3..751180da69 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -91,9 +91,10 @@ import { Authentication, HttpAuth, HttpOperation, + HttpOperationBody, + HttpOperationMultipartBody, HttpOperationParameters, HttpOperationResponse, - HttpOperationResponseBody, HttpStatusCodeRange, HttpStatusCodesEntry, MetadataInfo, @@ -129,6 +130,7 @@ import { sortWithJsonSchema } from "./json-schema-sorter/sorter.js"; import { createDiagnostic, reportDiagnostic } from "./lib.js"; import { OpenAPI2Document, + OpenAPI2FileSchema, OpenAPI2FormDataParameter, OpenAPI2HeaderDefinition, OpenAPI2OAuth2FlowType, @@ -718,7 +720,7 @@ export async function getOpenAPIForService( openapiResponse["x-ms-error-response"] = true; } const contentTypes: string[] = []; - let body: HttpOperationResponseBody | undefined; + let body: HttpOperationBody | HttpOperationMultipartBody | undefined; for (const data of response.responses) { if (data.headers && Object.keys(data.headers).length > 0) { openapiResponse.headers ??= {}; @@ -740,13 +742,7 @@ export async function getOpenAPIForService( } if (body) { - const isBinary = contentTypes.every((t) => isBinaryPayload(body!.type, t)); - openapiResponse.schema = isBinary - ? { type: "file" } - : getSchemaOrRef(body.type, { - visibility: Visibility.Read, - ignoreMetadataAnnotations: body.isExplicit && body.containsMetadataAnnotations, - }); + openapiResponse.schema = getSchemaForResponseBody(body, contentTypes); } for (const contentType of contentTypes) { @@ -756,6 +752,24 @@ export async function getOpenAPIForService( currentEndpoint.responses![statusCode] = openapiResponse; } + function getSchemaForResponseBody( + body: HttpOperationBody | HttpOperationMultipartBody, + contentTypes: string[] + ): OpenAPI2Schema | OpenAPI2FileSchema { + const isBinary = contentTypes.every((t) => isBinaryPayload(body!.type, t)); + if (isBinary) { + return { type: "file" }; + } + if (body.bodyKind === "multipart") { + // OpenAPI2 doesn't support multipart responses, so we just return a string schema + return { type: "string" }; + } + return getSchemaOrRef(body.type, { + visibility: Visibility.Read, + ignoreMetadataAnnotations: body.isExplicit && body.containsMetadataAnnotations, + }); + } + function getResponseHeader(prop: ModelProperty): OpenAPI2HeaderDefinition { const header: any = {}; populateParameter(header, prop, "header", { @@ -955,39 +969,81 @@ export async function getOpenAPIForService( } if (methodParams.body && !isVoidType(methodParams.body.type)) { - const isBinary = isBinaryPayload(methodParams.body.type, consumes); - const schemaContext = { - visibility, - ignoreMetadataAnnotations: - methodParams.body.isExplicit && methodParams.body.containsMetadataAnnotations, - }; - const schema = isBinary - ? { type: "string", format: "binary" } - : getSchemaOrRef(methodParams.body.type, schemaContext); - - if (currentConsumes.has("multipart/form-data")) { - const bodyModelType = methodParams.body.type; - // Assert, this should never happen. Rest library guard against that. - compilerAssert(bodyModelType.kind === "Model", "Body should always be a Model."); - if (bodyModelType) { - for (const param of bodyModelType.properties.values()) { - emitParameter(param, "formData", schemaContext, getJsonName(param)); - } + emitBodyParameters(methodParams.body, visibility); + } + } + + function emitBodyParameters( + body: HttpOperationBody | HttpOperationMultipartBody, + visibility: Visibility + ) { + switch (body.bodyKind) { + case "single": + emitSingleBodyParameters(body, visibility); + break; + case "multipart": + emitMultipartBodyParameters(body, visibility); + break; + } + } + + function emitSingleBodyParameters(body: HttpOperationBody, visibility: Visibility) { + const isBinary = isBinaryPayload(body.type, body.contentTypes); + const schemaContext = { + visibility, + ignoreMetadataAnnotations: body.isExplicit && body.containsMetadataAnnotations, + }; + const schema = isBinary + ? { type: "string", format: "binary" } + : getSchemaOrRef(body.type, schemaContext); + + if (currentConsumes.has("multipart/form-data")) { + const bodyModelType = body.type; + // Assert, this should never happen. Rest library guard against that. + compilerAssert(bodyModelType.kind === "Model", "Body should always be a Model."); + if (bodyModelType) { + for (const param of bodyModelType.properties.values()) { + emitParameter(param, "formData", schemaContext, getJsonName(param)); + } + } + } else if (body.property) { + emitParameter( + body.property, + "body", + { visibility, ignoreMetadataAnnotations: false }, + getJsonName(body.property), + schema + ); + } else { + currentEndpoint.parameters.push({ + name: "body", + in: "body", + schema, + required: true, + }); + } + } + + function emitMultipartBodyParameters(body: HttpOperationMultipartBody, visibility: Visibility) { + for (const [index, part] of body.parts.entries()) { + const partName = part.name ?? `part${index}`; + let schema = getFormDataSchema( + part.body.type, + { visibility, ignoreMetadataAnnotations: false }, + partName + ); + if (schema) { + if (part.multi) { + schema = { + type: "array", + items: schema.type === "file" ? { type: "string", format: "binary" } : schema, + }; } - } else if (methodParams.body.parameter) { - emitParameter( - methodParams.body.parameter, - "body", - { visibility, ignoreMetadataAnnotations: false }, - getJsonName(methodParams.body.parameter), - schema - ); - } else { currentEndpoint.parameters.push({ - name: "body", - in: "body", - schema, - required: true, + name: partName, + in: "formData", + required: !part.optional, + ...schema, }); } } @@ -1029,6 +1085,26 @@ export async function getOpenAPIForService( } } + function getParameter( + param: ModelProperty, + kind: OpenAPI2ParameterType, + schemaContext: SchemaContext, + name?: string, + typeOverride?: any + ) { + if (isNeverType(param.type)) { + return; + } + + const ph = getParamPlaceholder(param); + + // If the parameter already has a $ref, don't bother populating it + if (!("$ref" in ph)) { + populateParameter(ph, param, kind, schemaContext, name, typeOverride); + } + return ph; + } + function getSchemaForPrimitiveItems( type: Type, schemaContext: SchemaContext, diff --git a/packages/typespec-autorest/src/openapi2-document.ts b/packages/typespec-autorest/src/openapi2-document.ts index fba900eabd..b6d041394e 100644 --- a/packages/typespec-autorest/src/openapi2-document.ts +++ b/packages/typespec-autorest/src/openapi2-document.ts @@ -285,6 +285,18 @@ export type OpenAPI2Schema = Extensions & { "x-ms-mutability"?: string[]; }; +export type OpenAPI2FileSchema = { + type: "file"; + format?: string; + title?: string; + description?: string; + default?: unknown; + required?: string[]; + readonly?: boolean; + externalDocs?: OpenAPI2ExternalDocs; + example?: unknown; +}; + export type OpenAPI2ParameterType = OpenAPI2Parameter["in"]; export interface OpenAPI2HeaderDefinition { @@ -465,7 +477,7 @@ export interface OpenAPI2Response { /** A short description of the response. Commonmark syntax can be used for rich text representation */ description: string; /** A definition of the response structure. It can be a primitive, an array or an object. If this field does not exist, it means no content is returned as part of the response. As an extension to the Schema Object, its root type value may also be "file". This SHOULD be accompanied by a relevant produces mime-type. */ - schema?: OpenAPI2Schema; + schema?: OpenAPI2Schema | OpenAPI2FileSchema; /** A list of headers that are sent with the response. */ headers?: Record; /** An example of the response message. */ diff --git a/packages/typespec-autorest/test/multipart.test.ts b/packages/typespec-autorest/test/multipart.test.ts index 5310a6f263..f4ba1520c2 100644 --- a/packages/typespec-autorest/test/multipart.test.ts +++ b/packages/typespec-autorest/test/multipart.test.ts @@ -2,7 +2,105 @@ import { deepStrictEqual } from "assert"; import { describe, it } from "vitest"; import { openApiFor } from "./test-host.js"; -describe("typespec-autorest: multipart", () => { +it("model properties are spread into individual parameters", async () => { + const res = await openApiFor( + ` + model Form { name: HttpPart, profileImage: HttpPart } + op upload(@header contentType: "multipart/form-data", @multipartBody body: Form): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.parameters, [ + { + in: "formData", + name: "name", + required: true, + type: "string", + }, + { + in: "formData", + name: "profileImage", + required: true, + type: "file", + }, + ]); +}); + +it("part of type `bytes` produce `type: file`", async () => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody body: { profileImage: HttpPart }): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.parameters, [ + { + in: "formData", + name: "profileImage", + required: true, + type: "file", + }, + ]); +}); + +it("part of type `bytes[]` produce `type: array, items: { type: string, format: binary }`", async () => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody _: { profileImage: HttpPart[]}): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.parameters, [ + { + in: "formData", + name: "profileImage", + required: true, + type: "array", + items: { + type: "string", + format: "binary", + }, + }, + ]); +}); + +it("part of type `string` produce `type: string`", async () => { + const res = await openApiFor( + ` + op upload(@header contentType: "multipart/form-data", @multipartBody body: { name: HttpPart }): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.parameters, [ + { + in: "formData", + name: "name", + required: true, + type: "string", + }, + ]); +}); + +// https://github.com/Azure/typespec-azure/issues/3860 +it("part of type `object` produce `type: string`", async () => { + const res = await openApiFor( + ` + #suppress "@azure-tools/typespec-autorest/unsupported-multipart-type" "For test" + op upload(@header contentType: "multipart/form-data", @multipartBody _: { address: HttpPart<{city: string, street: string}>}): void; + ` + ); + const op = res.paths["/"].post; + deepStrictEqual(op.parameters, [ + { + in: "formData", + name: "address", + required: true, + type: "string", + }, + ]); +}); + +describe("legacy implicit form", () => { it("part of type `bytes` produce `type: file`", async () => { const res = await openApiFor( ` diff --git a/packages/typespec-azure-resource-manager/src/rules/arm-post-response-codes.ts b/packages/typespec-azure-resource-manager/src/rules/arm-post-response-codes.ts index 369b216eeb..b2711f1402 100644 --- a/packages/typespec-azure-resource-manager/src/rules/arm-post-response-codes.ts +++ b/packages/typespec-azure-resource-manager/src/rules/arm-post-response-codes.ts @@ -25,7 +25,10 @@ export const armPostResponseCodesRule = createRule({ if (response.responses.length > 1) { throw new Error("Multiple responses are not supported."); } - if (response.responses[0].body !== undefined) { + if ( + response.responses[0].body !== undefined && + response.responses[0].body.bodyKind === "single" + ) { return response.responses[0].body; } return undefined; diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 157895e26d..92e754d6be 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -125,7 +125,7 @@ function getSdkHttpParameters( const tspBody = httpOperation.parameters.body; // we add correspondingMethodParams after we create the type, since we need the info on the type const correspondingMethodParams: SdkModelPropertyType[] = []; - if (tspBody) { + if (tspBody && tspBody?.bodyKind !== "multipart") { // if there's a param on the body, we can just rely on getSdkHttpParameter if (tspBody.parameter && !isNeverOrVoidType(tspBody.parameter.type)) { const getParamResponse = diagnostics.pipe( diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 514e40a5cd..eb81fb8b94 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -252,7 +252,11 @@ function getSdkBasicServiceMethod< methodParameters.push(diagnostics.pipe(getSdkMethodParameter(context, param.param, operation))); } // body parameters - if (parameters.body?.parameter && !isNeverOrVoidType(parameters.body.parameter.type)) { + if ( + parameters.body?.bodyKind !== "multipart" && + parameters.body?.parameter && + !isNeverOrVoidType(parameters.body.parameter.type) + ) { methodParameters.push( diagnostics.pipe(getSdkMethodParameter(context, parameters.body?.parameter, operation)) ); From 1d9e6fb6ecde5e06a8b65f3d5d386f3917080113 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 22 May 2024 08:59:58 -0700 Subject: [PATCH 02/17] Fix --- core | 2 +- packages/typespec-autorest/src/openapi.ts | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/core b/core index a63a6f616c..54b2abee8a 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit a63a6f616c3a90e28f6ccb8ffddcb6642962b000 +Subproject commit 54b2abee8abb1740b74a86278372fc6012baaf73 diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index 751180da69..5cc23ff321 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -1085,26 +1085,6 @@ export async function getOpenAPIForService( } } - function getParameter( - param: ModelProperty, - kind: OpenAPI2ParameterType, - schemaContext: SchemaContext, - name?: string, - typeOverride?: any - ) { - if (isNeverType(param.type)) { - return; - } - - const ph = getParamPlaceholder(param); - - // If the parameter already has a $ref, don't bother populating it - if (!("$ref" in ph)) { - populateParameter(ph, param, kind, schemaContext, name, typeOverride); - } - return ph; - } - function getSchemaForPrimitiveItems( type: Type, schemaContext: SchemaContext, From 05b574ad2bd9ff1ba8a0badf6edaf7c6542b1242 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 22 May 2024 09:06:54 -0700 Subject: [PATCH 03/17] Create uptake-multipartv2-2024-4-22-16-3-12.md --- .chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.md diff --git a/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.md b/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.md new file mode 100644 index 0000000000..74d948dfd6 --- /dev/null +++ b/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@azure-tools/typespec-autorest" +--- + +Add support for new multipart constructs in http library From a33fac1e7958f13abc50ae9753267cda2f3430bc Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 22 May 2024 09:07:25 -0700 Subject: [PATCH 04/17] Create uptake-multipartv2-2024-4-22-16-3-12.2.md --- .chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md diff --git a/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md b/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md new file mode 100644 index 0000000000..25d4fe02c6 --- /dev/null +++ b/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md @@ -0,0 +1,7 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@azure-tools/typespec-azure-resource-manager" + - "@azure-tools/typespec-client-generator-core" +--- From 257ec9ac86bcbbef2c9e41ff03c8d70fa42cdf8c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 22 May 2024 12:06:52 -0700 Subject: [PATCH 05/17] fix --- core | 2 +- packages/typespec-azure-core/src/lro-info.ts | 4 ++-- packages/typespec-azure-core/test/test-host.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core b/core index 54b2abee8a..f999aaf622 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 54b2abee8abb1740b74a86278372fc6012baaf73 +Subproject commit f999aaf622a84ae00db80b6262a266c139cc0aee diff --git a/packages/typespec-azure-core/src/lro-info.ts b/packages/typespec-azure-core/src/lro-info.ts index 7cbc9ef951..1997d23486 100644 --- a/packages/typespec-azure-core/src/lro-info.ts +++ b/packages/typespec-azure-core/src/lro-info.ts @@ -137,8 +137,8 @@ export function getLroOperationInfo( } if (targetParameters.body) { const body = targetParameters.body; - if (body.parameter) { - targetProperties.set(body.parameter.name, body.parameter); + if (body.bodyKind === "single" && body.property) { + targetProperties.set(body.property.name, body.property); } else if (body.type.kind === "Model") { for (const [name, param] of getAllProperties(body.type)) { targetProperties.set(name, param); diff --git a/packages/typespec-azure-core/test/test-host.ts b/packages/typespec-azure-core/test/test-host.ts index 7a4fef050e..c2cfc60f02 100644 --- a/packages/typespec-azure-core/test/test-host.ts +++ b/packages/typespec-azure-core/test/test-host.ts @@ -102,7 +102,7 @@ export async function getSimplifiedOperations( params: { params: r.parameters.parameters.map(({ type, name }) => ({ type, name })), body: - r.parameters.body?.parameter?.name ?? + r.parameters.body?.property?.name ?? (r.parameters.body?.type?.kind === "Model" ? Array.from(r.parameters.body.type.properties.keys()) : undefined), From 1df54a68b554bc182768cbde5da7c8c55b0b5f64 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 22 May 2024 12:13:10 -0700 Subject: [PATCH 06/17] Update uptake-multipartv2-2024-4-22-16-3-12.2.md --- .chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md b/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md index 25d4fe02c6..84b508ab18 100644 --- a/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md +++ b/.chronus/changes/uptake-multipartv2-2024-4-22-16-3-12.2.md @@ -4,4 +4,5 @@ changeKind: internal packages: - "@azure-tools/typespec-azure-resource-manager" - "@azure-tools/typespec-client-generator-core" + - "@azure-tools/typespec-azure-core" --- From 6324fb4279114e0454f3d0935f818edec23f59f5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 23 May 2024 14:27:57 -0700 Subject: [PATCH 07/17] bump --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index f34ef31629..3f6193bb46 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit f34ef31629cbf6d3cb964bd263766c88895199bc +Subproject commit 3f6193bb46d6a5ac998f8f24610d9858a5ea88d0 From d20035eb6c7d1aa7ed2e893128011bce48dda7c9 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 24 May 2024 11:33:33 -0700 Subject: [PATCH 08/17] . --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 3f6193bb46..e6aa61c310 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 3f6193bb46d6a5ac998f8f24610d9858a5ea88d0 +Subproject commit e6aa61c310c5ccfc2ded8870d98dc9714a309d1e From fb1259f6efcbc5871821d67ea3ef55c388503fa1 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 3 Jun 2024 08:49:29 -0700 Subject: [PATCH 09/17] merge --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index e6aa61c310..77037e0d84 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit e6aa61c310c5ccfc2ded8870d98dc9714a309d1e +Subproject commit 77037e0d84e4be3e75dddff8a1c851256a93a38f From 43cee829d90773fafb71509653e19dee6f697067 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 3 Jun 2024 08:53:38 -0700 Subject: [PATCH 10/17] missing refs --- packages/typespec-client-generator-core/tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/typespec-client-generator-core/tsconfig.json b/packages/typespec-client-generator-core/tsconfig.json index 1aef16e920..b58c2a0526 100644 --- a/packages/typespec-client-generator-core/tsconfig.json +++ b/packages/typespec-client-generator-core/tsconfig.json @@ -3,7 +3,9 @@ "composite": true, "references": [ { "path": "../../core/packages/compiler/tsconfig.json" }, - { "path": "../../core/packages/rest/tsconfig.json" } + { "path": "../../core/packages/http/tsconfig.json" }, + { "path": "../typespec-azure-core/tsconfig.json" }, + { "path": "../typespec-azure-resource-manager/tsconfig.json" } ], "compilerOptions": { "outDir": "dist", From c92d90a6292974e00fc5810fa271f822fc4c869f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 3 Jun 2024 08:59:26 -0700 Subject: [PATCH 11/17] fix --- core | 2 +- packages/typespec-client-generator-core/src/package.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core b/core index 77037e0d84..23a6709abc 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 77037e0d84e4be3e75dddff8a1c851256a93a38f +Subproject commit 23a6709abc940cb281013cfcb48949e1480ca771 diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 13a03bf1c9..53fd9472cc 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -252,11 +252,11 @@ function getSdkBasicServiceMethod< // body parameters if ( parameters.body?.bodyKind !== "multipart" && - parameters.body?.parameter && - !isNeverOrVoidType(parameters.body.parameter.type) + parameters.body?.property && + !isNeverOrVoidType(parameters.body.property.type) ) { methodParameters.push( - diagnostics.pipe(getSdkMethodParameter(context, parameters.body?.parameter, operation)) + diagnostics.pipe(getSdkMethodParameter(context, parameters.body?.property, operation)) ); } else if (parameters.body && !isNeverOrVoidType(parameters.body.type)) { if (parameters.body.type.kind === "Model") { From f6d05b78392c316f4c65f2e1e8ed3b4ec3817f2d Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 3 Jun 2024 09:13:39 -0700 Subject: [PATCH 12/17] Address CR comment --- .../src/rules/arm-post-response-codes.ts | 13 +++---- .../rules/arm-post-response-codes.test.ts | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/packages/typespec-azure-resource-manager/src/rules/arm-post-response-codes.ts b/packages/typespec-azure-resource-manager/src/rules/arm-post-response-codes.ts index b2711f1402..2e1063b49c 100644 --- a/packages/typespec-azure-resource-manager/src/rules/arm-post-response-codes.ts +++ b/packages/typespec-azure-resource-manager/src/rules/arm-post-response-codes.ts @@ -1,7 +1,11 @@ import { Program, createRule } from "@typespec/compiler"; import { getLroMetadata } from "@azure-tools/typespec-azure-core"; -import { HttpOperationBody, HttpOperationResponse } from "@typespec/http"; +import { + HttpOperationBody, + HttpOperationMultipartBody, + HttpOperationResponse, +} from "@typespec/http"; import { ArmResourceOperation } from "../operations.js"; import { getArmResources } from "../resource.js"; @@ -20,15 +24,12 @@ export const armPostResponseCodesRule = createRule({ create(context) { function getResponseBody( response: HttpOperationResponse | undefined - ): HttpOperationBody | undefined { + ): HttpOperationBody | HttpOperationMultipartBody | undefined { if (response === undefined) return undefined; if (response.responses.length > 1) { throw new Error("Multiple responses are not supported."); } - if ( - response.responses[0].body !== undefined && - response.responses[0].body.bodyKind === "single" - ) { + if (response.responses[0].body !== undefined) { return response.responses[0].body; } return undefined; diff --git a/packages/typespec-azure-resource-manager/test/rules/arm-post-response-codes.test.ts b/packages/typespec-azure-resource-manager/test/rules/arm-post-response-codes.test.ts index 1651b020eb..2569c581d1 100644 --- a/packages/typespec-azure-resource-manager/test/rules/arm-post-response-codes.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/arm-post-response-codes.test.ts @@ -53,6 +53,41 @@ it("Emits a warning for a synchronous post operation that does not contain the a }); }); +it("Emits a warning for a synchronous post operation that does not contain the appropriate response codes and use multipart/mixed", async () => { + await tester + .expect( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso; + + model Employee is ProxyResource<{}> { + @pattern("^[a-zA-Z0-9-]{3,24}$") + @key("employeeName") + @path + @segment("employees") + name: string; + } + + @armResourceOperations + interface Employees { + @post + @armResourceAction(Employee) + hire(...ApiVersionParameter): { + @statusCode _: 203; + @header contentType: "multipart/mixed"; + @multipartBody result: [HttpPart] + } | ErrorResponse; + } + ` + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-resource-manager/arm-post-operation-response-codes", + message: + "Synchronous post operations must have a 200 or 204 response and a default response. They must not have any other responses.", + }); +}); + it("Does not emit a warning for a synchronous post operation that contains the 200 and default response codes", async () => { await tester .expect( From cac0d765a905c4baa09fe78d04a7c95b20770e9c Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 3 Jun 2024 12:52:55 -0700 Subject: [PATCH 13/17] bump core --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 23a6709abc..0a08dbcc5d 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 23a6709abc940cb281013cfcb48949e1480ca771 +Subproject commit 0a08dbcc5de2f7db8f4d9eb9bc2816f77e3e70d1 From df150aaf07b2273dddd5dbb6b4311e5f0a7fa400 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 3 Jun 2024 13:31:10 -0700 Subject: [PATCH 14/17] fix --- .../typespec-azure-playground-website/samples/azure-core.tsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typespec-azure-playground-website/samples/azure-core.tsp b/packages/typespec-azure-playground-website/samples/azure-core.tsp index 9bae9beb73..ed597517a2 100644 --- a/packages/typespec-azure-playground-website/samples/azure-core.tsp +++ b/packages/typespec-azure-playground-website/samples/azure-core.tsp @@ -256,7 +256,7 @@ interface WidgetParts { reorderParts is Operations.LongRunningResourceCollectionAction< WidgetPart, WidgetPartReorderRequest, - TypeSpec.Http.AcceptedResponse + never >; } From ef4df00829ef4a91348fd4de5a5044fd5f7eedaf Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 3 Jun 2024 15:25:28 -0700 Subject: [PATCH 15/17] Use main --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 0a08dbcc5d..40df1ec9a3 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 0a08dbcc5de2f7db8f4d9eb9bc2816f77e3e70d1 +Subproject commit 40df1ec9a307a6bde664b2ad08c4f823d6956cc0 From 50501575010cee4d93b45ddde15feaa1c375a64a Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 3 Jun 2024 15:26:33 -0700 Subject: [PATCH 16/17] fix --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89a9ba2673..5db8b370b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,9 @@ importers: '@typescript-eslint/utils': specifier: ^7.9.0 version: 7.9.0(eslint@8.57.0)(typescript@5.4.5) + '@vitest/coverage-v8': + specifier: ^1.6.0 + version: 1.6.0(vitest@1.6.0) c8: specifier: ^9.1.0 version: 9.1.0 From ce50051ba3bc5c0bfe5a0f3ba50155f7f24ae33b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 3 Jun 2024 15:44:03 -0700 Subject: [PATCH 17/17] use dotnet 8 --- eng/pipelines/templates/install.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/templates/install.yml b/eng/pipelines/templates/install.yml index 0717d63a82..da7647b03f 100644 --- a/eng/pipelines/templates/install.yml +++ b/eng/pipelines/templates/install.yml @@ -9,7 +9,7 @@ parameters: steps: - task: UseDotNet@2 inputs: - version: 6.0.x + version: 8.0.x - task: NodeTool@0 inputs: