From 93a434374a96074e1c0d085b6ac6779b8f376e59 Mon Sep 17 00:00:00 2001 From: Chenjie Shi Date: Mon, 1 Apr 2024 08:24:32 +0800 Subject: [PATCH] TCGC common layer refine (#517) --- .../fix_tcgc_type_issue-2024-2-26-17-43-28.md | 7 + .../src/http.ts | 69 +++-- .../src/interfaces.ts | 2 +- .../src/internal-utils.ts | 3 +- .../src/package.ts | 47 ++- .../src/public-utils.ts | 17 +- .../src/types.ts | 2 +- .../test/package.test.ts | 288 +++++++++++++++--- .../test/public-utils.test.ts | 2 +- .../test/types.test.ts | 53 ++++ 10 files changed, 402 insertions(+), 88 deletions(-) create mode 100644 .chronus/changes/fix_tcgc_type_issue-2024-2-26-17-43-28.md diff --git a/.chronus/changes/fix_tcgc_type_issue-2024-2-26-17-43-28.md b/.chronus/changes/fix_tcgc_type_issue-2024-2-26-17-43-28.md new file mode 100644 index 0000000000..c9526950cf --- /dev/null +++ b/.chronus/changes/fix_tcgc_type_issue-2024-2-26-17-43-28.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +fix wrong union `generatedName` flag and refine templated model naming \ No newline at end of file diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 9b24fc949a..1d0a786b26 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -20,6 +20,7 @@ import { isPathParam, isQueryParam, } from "@typespec/http"; +import { camelCase } from "change-case"; import { CollectionFormat, SdkBodyParameter, @@ -29,6 +30,7 @@ import { SdkHttpResponse, SdkMethodParameter, SdkModelPropertyType, + SdkModelType, SdkParameter, SdkPathParameter, SdkQueryParameter, @@ -123,10 +125,14 @@ function getSdkHttpParameters( if (getParamResponse.kind !== "body") throw new Error("blah"); retval.bodyParam = getParamResponse; } else { + const type = diagnostics.pipe( + getClientTypeWithDiagnostics(context, tspBody.type, httpOperation.operation) + ); + const name = camelCase((type as { name: string }).name ?? "body"); retval.bodyParam = { kind: "body", - name: "body", - nameInClient: "body", + name, + nameInClient: name, isGeneratedName: true, description: getDocHelper(context, tspBody.type).description, details: getDocHelper(context, tspBody.type).details, @@ -135,9 +141,7 @@ function getSdkHttpParameters( defaultContentType: "application/json", // actual content type info is added later isApiVersionParam: false, apiVersions: getAvailableApiVersions(context, tspBody.type), - type: diagnostics.pipe( - getClientTypeWithDiagnostics(context, tspBody.type, httpOperation.operation) - ), + type, optional: false, nullable: isNullable(tspBody.type), correspondingMethodParams, @@ -393,33 +397,54 @@ export function getCorrespondingMethodParams( serviceParam: SdkHttpParameter ): SdkModelPropertyType[] { if (serviceParam.isApiVersionParam) { - if (!context.__api_version_parameter) throw new Error("No api version on the client"); + if (!context.__api_version_parameter) { + const apiVersionParam = methodParameters.find((x) => x.name.includes("apiVersion")); + if (!apiVersionParam) { + throw new Error("Can't find corresponding api version parameter"); + } + if (apiVersionParam.type.kind === "model") throw new Error(apiVersionParam.type.name); + throw new Error(apiVersionParam.type.kind); + } return [context.__api_version_parameter]; } const correspondingMethodParameter = methodParameters.find((x) => x.name === serviceParam.name); if (correspondingMethodParameter) { return [correspondingMethodParameter]; } - function paramInProperties(param: SdkModelPropertyType, type: SdkType): boolean { - if (type.kind !== "model") return false; - return Array.from(type.properties.values()) - .filter((x) => x.kind === "property") - .map((x) => x.name) - .includes(param.name); - } + const serviceParamType = serviceParam.type; if (serviceParam.kind === "body" && serviceParamType.kind === "model") { - // Here we have a spread body parameter - const correspondingProperties = methodParameters.filter((x) => - paramInProperties(x, serviceParamType) - ); - const bodyPropertyNames = serviceParamType.properties.filter((x) => - paramInProperties(x, serviceParamType) + const serviceParamPropertyNames = Array.from(serviceParamType.properties.values()) + .filter((x) => x.kind === "property") + .map((x) => x.name); + // Here we have a spread method parameter + + // easy case is if the spread method parameter directly has the entire body as a property + const directBodyProperty = methodParameters + .map((x) => x.type) + .filter((x): x is SdkModelType => x.kind === "model") + .flatMap((x) => x.properties) + .find((x) => x.type === serviceParamType); + if (directBodyProperty) return [directBodyProperty]; + let correspondingProperties: SdkModelPropertyType[] = methodParameters.filter((x) => + serviceParamPropertyNames.includes(x.name) ); - if (correspondingProperties.length !== bodyPropertyNames.length) { - throw new Error("Can't find corresponding properties for spread body parameter"); + for (const methodParam of methodParameters) { + const methodParamIterable = + methodParam.type.kind === "model" ? methodParam.type.properties : [methodParam]; + correspondingProperties = correspondingProperties.concat( + methodParamIterable.filter( + (x) => + serviceParamPropertyNames.includes(x.name) && + !correspondingProperties.find((e) => e.name === x.name) + ) + ); } - return correspondingProperties; + if (correspondingProperties.length === serviceParamType.properties.length) + return correspondingProperties; + throw new Error( + `Can't find corresponding parameter for ${serviceParam.name} out of ${methodParameters.map((m) => m.name).join(", ")}` + ); } for (const methodParam of methodParameters) { if (methodParam.type.kind === "model") { diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 0a7eff0059..a3a3d7f0f4 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -256,7 +256,7 @@ export interface SdkEnumValueType extends SdkTypeBase { name: string; value: string | number; enumType: SdkEnumType; - valueType: SdkType; + valueType: SdkBuiltInType; description?: string; details?: string; } diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 5995ad4a23..f8d8d0007e 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -1,7 +1,6 @@ import { getUnionAsEnum } from "@azure-tools/typespec-azure-core"; import { Model, - ModelProperty, Namespace, Operation, Program, @@ -82,7 +81,7 @@ export function getClientNamespaceStringHelper( */ export function updateWithApiVersionInformation( context: TCGCContext, - type: ModelProperty + type: { name: string } ): { isApiVersionParam: boolean; clientDefaultValue?: unknown; diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 2a94d1d98e..7a2989b6cb 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -1,14 +1,17 @@ import { getLroMetadata, getPagedResult } from "@azure-tools/typespec-azure-core"; import { Diagnostic, + Model, ModelProperty, Operation, createDiagnosticCollector, getNamespaceFullName, getService, + isKey, } from "@typespec/compiler"; import { getServers } from "@typespec/http"; import { resolveVersions } from "@typespec/versioning"; +import { camelCase } from "change-case"; import { getAccess, listClients, @@ -53,6 +56,7 @@ import { getHashForType, getSdkTypeBaseHelper, isNullable, + updateWithApiVersionInformation, } from "./internal-utils.js"; import { createDiagnostic } from "./lib.js"; import { @@ -218,9 +222,24 @@ function getSdkBasicServiceMethod< ): [SdkServiceMethod, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); // when we spread, all of the inputtable properties of our model get flattened onto the method - const methodParameters = Array.from(operation.parameters.properties.values()) - .map((x) => diagnostics.pipe(getSdkMethodParameter(context, x))) - .filter((x): x is SdkMethodParameter => x.kind === "method"); + const methodParameters: SdkMethodParameter[] = []; + const spreadModelNames: string[] = []; + for (const prop of operation.parameters.properties.values()) { + if (prop.sourceProperty?.model?.name && !isKey(context.program, prop.sourceProperty)) { + if (!spreadModelNames.includes(prop.sourceProperty.model.name)) { + spreadModelNames.push(prop.sourceProperty.model.name); + methodParameters.push( + diagnostics.pipe(getSdkMethodParameter(context, prop.sourceProperty.model)) + ); + } + } else { + const methodParameter = diagnostics.pipe(getSdkMethodParameter(context, prop)); + if (methodParameter.kind === "method") { + methodParameters.push(methodParameter); + } + } + } + // if there's an api version parameter, we want to bubble it up to the client // we don't want it on the method level, but we will keep it on the service operation level const apiVersionParam = methodParameters.find((x) => x.isApiVersionParam); @@ -331,9 +350,29 @@ function getSdkInitializationType< function getSdkMethodParameter( context: TCGCContext, - type: ModelProperty + type: ModelProperty | Model ): [SdkMethodParameter, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); + if (type.kind === "Model") { + const libraryName = getLibraryName(context, type); + const name = camelCase(libraryName ?? "body"); + const propertyType = diagnostics.pipe(getClientTypeWithDiagnostics(context, type)); + return diagnostics.wrap({ + kind: "method", + description: getDocHelper(context, type).description, + details: getDocHelper(context, type).details, + apiVersions: getAvailableApiVersions(context, type), + type: propertyType, + nameInClient: name, + name, + isGeneratedName: Boolean(libraryName), + optional: false, + nullable: false, + discriminator: false, + serializedName: name, + ...updateWithApiVersionInformation(context, type), + }); + } return diagnostics.wrap({ ...diagnostics.pipe(getSdkModelPropertyType(context, type)), kind: "method", diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index b7c2b9b6ac..c4a72aa314 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -19,7 +19,6 @@ import { } from "@typespec/compiler"; import { HttpOperation, - HttpOperationParameter, getHeaderFieldName, getHttpOperation, getPathParamName, @@ -62,12 +61,10 @@ export function getDefaultApiVersion( * @param parameter * @returns */ -export function isApiVersion( - context: TCGCContext, - parameter: HttpOperationParameter | ModelProperty -): boolean { +export function isApiVersion(context: TCGCContext, type: { name: string }): boolean { return ( - parameter.name.toLowerCase() === "apiversion" || parameter.name.toLowerCase() === "api-version" + type.name.toLowerCase().includes("apiversion") || + type.name.toLowerCase().includes("api-version") ); } @@ -174,7 +171,13 @@ export function getLibraryName( // 5. if type is derived from template and name is the same as template, add template parameters' name as suffix if (typeof type.name === "string" && type.kind === "Model" && type.templateMapper?.args) { - return type.name + type.templateMapper.args.map((arg) => (arg as Model).name).join(""); + return ( + type.name + + type.templateMapper.args + .filter((arg): arg is Model => arg.kind === "Model" && arg.name.length > 0) + .map((arg) => pascalCase(arg.name)) + .join("") + ); } return typeof type.name === "string" ? type.name : ""; diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 2dc46d6a34..fa88ebdfcd 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -896,7 +896,7 @@ function getSdkCredentialType( values: credentialTypes, nullable: false, name: createGeneratedName(client.service, "CredentialUnion"), - isGeneratedName: false, + isGeneratedName: true, }; } return credentialTypes[0]; diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index b82cec7cce..2e9483727c 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -223,6 +223,8 @@ describe("typespec-client-generator-core: package", () => { strictEqual(credentialParam.onClient, true); strictEqual(credentialParam.optional, false); strictEqual(credentialParam.type.kind, "union"); + strictEqual(credentialParam.type.name, "ServiceCredentialUnion"); + strictEqual(credentialParam.type.isGeneratedName, true); strictEqual(credentialParam.type.values.length, 2); const schemes = credentialParam.type.values .filter((v): v is SdkCredentialType => v.kind === "credential") @@ -995,11 +997,11 @@ describe("typespec-client-generator-core: package", () => { strictEqual(method.parameters.length, 1); const pathMethod = method.parameters[0]; strictEqual(pathMethod.kind, "method"); - strictEqual(pathMethod.name, "name"); + strictEqual(pathMethod.name, "nameParameter"); strictEqual(pathMethod.optional, false); strictEqual(pathMethod.onClient, false); strictEqual(pathMethod.isApiVersionParam, false); - strictEqual(pathMethod.type.kind, "string"); + strictEqual(pathMethod.type.kind, "model"); strictEqual(pathMethod.nullable, false); const serviceOperation = method.operation; @@ -1016,7 +1018,7 @@ describe("typespec-client-generator-core: package", () => { strictEqual(pathParam.urlEncode, true); strictEqual(pathParam.nullable, false); strictEqual(pathParam.correspondingMethodParams.length, 1); - deepStrictEqual(pathParam.correspondingMethodParams[0], pathMethod); + deepStrictEqual(pathParam.correspondingMethodParams[0], pathMethod.type.properties[0]); }); it("header basic", async () => { @@ -1357,13 +1359,13 @@ describe("typespec-client-generator-core: package", () => { strictEqual(method.kind, "basic"); strictEqual(method.parameters.length, 2); - const methodParam = method.parameters.find((x) => x.name === "key"); + const methodParam = method.parameters.find((x) => x.name === "input"); ok(methodParam); strictEqual(methodParam.kind, "method"); strictEqual(methodParam.optional, false); strictEqual(methodParam.onClient, false); strictEqual(methodParam.isApiVersionParam, false); - strictEqual(methodParam.type.kind, "string"); + strictEqual(methodParam.type.kind, "model"); const contentTypeParam = method.parameters.find((x) => x.name === "contentType"); ok(contentTypeParam); @@ -1384,11 +1386,7 @@ describe("typespec-client-generator-core: package", () => { const correspondingMethodParams = bodyParameter.correspondingMethodParams; strictEqual(correspondingMethodParams.length, 1); - strictEqual( - bodyParameter.type.properties[0].nameInClient, //eslint-disable-line deprecation/deprecation - correspondingMethodParams[0].nameInClient //eslint-disable-line deprecation/deprecation - ); - strictEqual(bodyParameter.type.properties[0].name, correspondingMethodParams[0].name); + strictEqual(bodyParameter.type, correspondingMethodParams[0].type); }); it("body alias spread", async () => { @@ -1446,6 +1444,68 @@ describe("typespec-client-generator-core: package", () => { strictEqual(bodyParameter.type.properties[0].name, correspondingMethodParams[0].name); }); + it("spread with discriminate type with implicit property", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + + @discriminator("kind") + model Pet { + name?: string; + } + + model Dog { + kind: "dog"; + } + + model Cat { + kind: "cat"; + } + + op test(...Pet): void; + `); + const sdkPackage = runner.context.experimental_sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(sdkPackage.models.length, 1); + strictEqual(method.name, "test"); + strictEqual(method.kind, "basic"); + strictEqual(method.parameters.length, 2); + + const methodParam = method.parameters.find((x) => x.name === "pet"); + ok(methodParam); + strictEqual(methodParam.kind, "method"); + strictEqual(methodParam.optional, false); + strictEqual(methodParam.onClient, false); + strictEqual(methodParam.isApiVersionParam, false); + strictEqual(methodParam.type.kind, "model"); + + const contentTypeMethodParam = method.parameters.find((x) => x.name === "contentType"); + ok(contentTypeMethodParam); + strictEqual(contentTypeMethodParam.clientDefaultValue, undefined); + strictEqual(contentTypeMethodParam.type.kind, "constant"); + + const serviceOperation = method.operation; + const bodyParameter = serviceOperation.bodyParam; + ok(bodyParameter); + strictEqual(bodyParameter.kind, "body"); + deepStrictEqual(bodyParameter.contentTypes, ["application/json"]); + strictEqual(bodyParameter.defaultContentType, "application/json"); + strictEqual(bodyParameter.onClient, false); + strictEqual(bodyParameter.optional, false); + strictEqual(bodyParameter.type.kind, "model"); + strictEqual(bodyParameter.type.properties.length, 2); + //eslint-disable-next-line deprecation/deprecation + strictEqual(bodyParameter.type.properties[0].nameInClient, "kind"); + strictEqual(bodyParameter.type.properties[0].name, "kind"); + //eslint-disable-next-line deprecation/deprecation + strictEqual(bodyParameter.type.properties[1].nameInClient, "name"); + strictEqual(bodyParameter.type.properties[1].name, "name"); + + const correspondingMethodParams = bodyParameter.correspondingMethodParams; + strictEqual(correspondingMethodParams.length, 1); + strictEqual(bodyParameter.type, correspondingMethodParams[0].type); + }); + it("parameter grouping", async () => { await runner.compile(`@server("http://localhost:3000", "endpoint") @service({}) @@ -1993,18 +2053,18 @@ describe("typespec-client-generator-core: package", () => { const method = getServiceMethodOfClient(sdkPackage); strictEqual(method.name, "create"); strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 5); + strictEqual(method.parameters.length, 3); deepStrictEqual( method.parameters.map((x) => x.name), - ["id", "weight", "color", "contentType", "accept"] + ["widget", "contentType", "accept"] ); const bodyParameter = method.operation.bodyParam; ok(bodyParameter); strictEqual(bodyParameter.kind, "body"); //eslint-disable-next-line deprecation/deprecation - strictEqual(bodyParameter.nameInClient, "body"); - strictEqual(bodyParameter.name, "body"); + strictEqual(bodyParameter.nameInClient, "widget"); + strictEqual(bodyParameter.name, "widget"); strictEqual(bodyParameter.onClient, false); strictEqual(bodyParameter.optional, false); strictEqual(bodyParameter.type.kind, "model"); @@ -2104,41 +2164,17 @@ describe("typespec-client-generator-core: package", () => { const method = getServiceMethodOfClient(sdkPackage); strictEqual(method.name, "update"); strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 5); - - const methodParamId = method.parameters[0]; - strictEqual(methodParamId.kind, "method"); - //eslint-disable-next-line deprecation/deprecation - strictEqual(methodParamId.nameInClient, "id"); - strictEqual(methodParamId.name, "id"); - strictEqual(methodParamId.optional, false); - strictEqual(methodParamId.onClient, false); - strictEqual(methodParamId.isApiVersionParam, false); - strictEqual(methodParamId.type.kind, "string"); - - const methodParamWeight = method.parameters[1]; - strictEqual(methodParamWeight.kind, "method"); - //eslint-disable-next-line deprecation/deprecation - strictEqual(methodParamWeight.nameInClient, "weight"); - strictEqual(methodParamWeight.name, "weight"); - strictEqual(methodParamWeight.optional, false); - strictEqual(methodParamWeight.onClient, false); - strictEqual(methodParamWeight.isApiVersionParam, false); - strictEqual(methodParamWeight.type.kind, "int32"); - - const methodParamColor = method.parameters[2]; - strictEqual(methodParamColor.kind, "method"); - //eslint-disable-next-line deprecation/deprecation - strictEqual(methodParamColor.nameInClient, "color"); - strictEqual(methodParamColor.name, "color"); - strictEqual(methodParamColor.optional, false); - strictEqual(methodParamColor.onClient, false); - strictEqual(methodParamColor.isApiVersionParam, false); - strictEqual(methodParamColor.type.kind, "enum"); - strictEqual(methodParamColor.type.values[0].value, "red"); - strictEqual(methodParamColor.type.values[0].valueType.kind, "string"); - strictEqual(methodParamColor.type.values[1].value, "blue"); - strictEqual(methodParamColor.type.values[1].valueType.kind, "string"); + strictEqual(method.parameters.length, 3); + + const methodParam = method.parameters[0]; + strictEqual(methodParam.kind, "method"); + //eslint-disable-next-line deprecation/deprecation + strictEqual(methodParam.nameInClient, "widget"); + strictEqual(methodParam.name, "widget"); + strictEqual(methodParam.optional, false); + strictEqual(methodParam.onClient, false); + strictEqual(methodParam.isApiVersionParam, false); + strictEqual(methodParam.type.kind, "model"); const methodContentTypeParam = method.parameters.find((x) => x.name === "contentType"); ok(methodContentTypeParam); @@ -2192,7 +2228,7 @@ describe("typespec-client-generator-core: package", () => { strictEqual(operationAcceptParam.optional, false); const correspondingMethodParams = bodyParameter.correspondingMethodParams.map((x) => x.name); - deepStrictEqual(correspondingMethodParams, ["weight", "color"]); + deepStrictEqual(correspondingMethodParams, ["widget"]); deepStrictEqual( bodyParameter.type.properties.map((p) => p.name), ["id", "weight", "color"] @@ -2823,6 +2859,158 @@ describe("typespec-client-generator-core: package", () => { ok(clientRequestIdProperty); strictEqual(clientRequestIdProperty.kind, "header"); }); + + it("multiple spread", async () => { + await runner.compile(` + @service({ + title: "Pet Store Service", + }) + namespace PetStore; + using TypeSpec.Rest.Resource; + + @error + model PetStoreError { + code: int32; + message: string; + } + + @resource("pets") + model Pet { + @key("petId") + id: int32; + } + + @resource("checkups") + model Checkup { + @key("checkupId") + id: int32; + + vetName: string; + notes: string; + } + + interface PetCheckups + extends ExtensionResourceCreateOrUpdate, + ExtensionResourceList {} + `); + const sdkPackage = runner.context.experimental_sdkPackage; + strictEqual(sdkPackage.models.length, 4); + deepStrictEqual( + sdkPackage.models.map((x) => x.name).sort(), + ["CheckupCollectionWithNextLink", "Checkup", "PetStoreError", "CheckupUpdate"].sort() + ); + const createOrUpdate = sdkPackage.clients[0].methods[0]; + strictEqual(createOrUpdate.kind, "basic"); + strictEqual(createOrUpdate.name, "createOrUpdate"); + strictEqual(createOrUpdate.parameters.length, 5); + strictEqual(createOrUpdate.parameters[0].name, "petId"); + strictEqual(createOrUpdate.parameters[1].name, "checkupId"); + strictEqual(createOrUpdate.parameters[2].name, "resource"); + strictEqual(createOrUpdate.parameters[2].type.kind, "model"); + strictEqual(createOrUpdate.parameters[2].type.name, "CheckupUpdate"); + strictEqual(createOrUpdate.parameters[3].name, "contentType"); + strictEqual(createOrUpdate.parameters[4].name, "accept"); + + const opParams = createOrUpdate.operation.parameters; + strictEqual(opParams.length, 4); + ok(opParams.find((x) => x.kind === "path" && x.serializedName === "petId")); + ok(opParams.find((x) => x.kind === "path" && x.serializedName === "checkupId")); + ok(opParams.find((x) => x.kind === "header" && x.serializedName === "Content-Type")); + ok(opParams.find((x) => x.kind === "header" && x.serializedName === "Accept")); + strictEqual(createOrUpdate.operation.responses.size, 2); + const response200 = createOrUpdate.operation.responses.get(200); + ok(response200); + ok(response200.type); + strictEqual(response200.type.kind, "model"); + strictEqual(response200.type.name, "Checkup"); + const response201 = createOrUpdate.operation.responses.get(201); + ok(response201); + ok(response201.type); + deepStrictEqual(response200.type, response201?.type); + }); + it("spread with @body in model", async () => { + await runner.compileWithBuiltInService(` + model Shelf { + name: string; + theme?: string; + } + model CreateShelfRequest { + @body + body: Shelf; + } + op createShelf(...CreateShelfRequest): Shelf; + `); + const method = getServiceMethodOfClient(runner.context.experimental_sdkPackage); + const models = runner.context.experimental_sdkPackage.models; + strictEqual(models.length, 1); + const shelfModel = models.find((x) => x.name === "Shelf"); + ok(shelfModel); + strictEqual(method.parameters.length, 3); + const createShelfRequest = method.parameters[0]; + strictEqual(createShelfRequest.kind, "method"); + strictEqual(createShelfRequest.name, "createShelfRequest"); + strictEqual(createShelfRequest.optional, false); + strictEqual(createShelfRequest.isGeneratedName, true); + strictEqual(createShelfRequest.type.kind, "model"); + strictEqual(createShelfRequest.type.properties.length, 1); + deepStrictEqual(createShelfRequest.type.properties[0].type, shelfModel); + const contentTypeMethoParam = method.parameters.find((x) => x.name === "contentType"); + ok(contentTypeMethoParam); + const acceptMethodParam = method.parameters.find((x) => x.name === "accept"); + ok(acceptMethodParam); + + const op = method.operation; + strictEqual(op.parameters.length, 2); + ok( + op.parameters.find( + (x) => + x.kind === "header" && + x.serializedName === "Content-Type" && + x.correspondingMethodParams[0] === contentTypeMethoParam + ) + ); + ok( + op.parameters.find( + (x) => + x.kind === "header" && + x.serializedName === "Accept" && + x.correspondingMethodParams[0] === acceptMethodParam + ) + ); + + const bodyParam = op.bodyParam; + ok(bodyParam); + strictEqual(bodyParam.kind, "body"); + strictEqual(bodyParam.name, "body"); + strictEqual(bodyParam.optional, false); + strictEqual(bodyParam.isGeneratedName, false); + deepStrictEqual(bodyParam.type, shelfModel); + deepStrictEqual(bodyParam.correspondingMethodParams, createShelfRequest.type.properties); + }); + }); + describe("versioning", () => { + it("define own api version param", async () => { + await runner.compileWithBuiltInService(` + model ApiVersionParam { + @header apiVersion: Versions; + } + + enum Versions { + v1, v2 + } + + op getPet(...ApiVersionParam): void; + `); + const sdkPackage = runner.context.experimental_sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.operation.parameters.length, 1); + const apiVersionParam = method.operation.parameters[0]; + strictEqual(apiVersionParam.kind, "header"); + strictEqual(apiVersionParam.serializedName, "api-version"); + strictEqual(apiVersionParam.name, "apiVersion"); + strictEqual(apiVersionParam.onClient, true); + strictEqual(apiVersionParam.isApiVersionParam, true); + }); }); }); diff --git a/packages/typespec-client-generator-core/test/public-utils.test.ts b/packages/typespec-client-generator-core/test/public-utils.test.ts index 704d3e1bca..ef107cac16 100644 --- a/packages/typespec-client-generator-core/test/public-utils.test.ts +++ b/packages/typespec-client-generator-core/test/public-utils.test.ts @@ -170,7 +170,7 @@ describe("typespec-client-generator-core: public-utils", () => { it("not api version param", async () => { const { func } = (await runner.compile(` - @test op func(@path notApiVersion: string): void; + @test op func(@path foo: string): void; `)) as { func: Operation }; const pathParam = ignoreDiagnostics(getHttpOperation(runner.context.program, func)).parameters diff --git a/packages/typespec-client-generator-core/test/types.test.ts b/packages/typespec-client-generator-core/test/types.test.ts index ca0eab7aca..02c4d984a9 100644 --- a/packages/typespec-client-generator-core/test/types.test.ts +++ b/packages/typespec-client-generator-core/test/types.test.ts @@ -2983,6 +2983,59 @@ describe("typespec-client-generator-core: types", () => { strictEqual(errorResponse.isFormDataType, false); ok((errorResponse.usage & UsageFlags.MultipartFormData) === 0); }); + + it("expands model into formData parameters", async function () { + await runner.compileWithBuiltInService(` + @doc("A widget.") + model Widget { + @key("widgetName") + name: string; + displayName: string; + description: string; + color: string; + } + + model WidgetForm is Widget { + @header("content-type") + contentType: "multipart/form-data"; + } + + @route("/widgets") + interface Widgets { + @route(":upload") + @post + upload(...WidgetForm): Widget; + } + `); + const formDataMethod = runner.context.experimental_sdkPackage.clients[0].methods[0]; + strictEqual(formDataMethod.kind, "basic"); + strictEqual(formDataMethod.name, "upload"); + strictEqual(formDataMethod.parameters.length, 3); + + const widgetFormParam = formDataMethod.parameters.find((x) => x.name === "widgetForm"); + ok(widgetFormParam); + ok(formDataMethod.parameters.find((x) => x.name === "accept")); + strictEqual(formDataMethod.parameters[0].name, "name"); + strictEqual(formDataMethod.parameters[0].type.kind, "string"); + strictEqual(formDataMethod.parameters[1].name, "widgetForm"); + strictEqual(formDataMethod.parameters[1].type.kind, "model"); + strictEqual(formDataMethod.parameters[1].type.name, "WidgetForm"); + + const formDataOp = formDataMethod.operation; + strictEqual(formDataOp.parameters.length, 2); + ok(formDataOp.parameters.find((x) => x.name === "accept" && x.kind === "header")); + ok(formDataOp.parameters.find((x) => x.name === "contentType" && x.kind === "header")); + + const formDataBodyParam = formDataOp.bodyParam; + ok(formDataBodyParam); + strictEqual(formDataBodyParam.type.kind, "model"); + strictEqual(formDataBodyParam.type.name, "Widget"); + strictEqual(formDataBodyParam.correspondingMethodParams.length, 4); + deepStrictEqual( + formDataBodyParam.correspondingMethodParams.map((x) => x.name).sort(), + ["color", "description", "displayName", "name"].sort() + ); + }); }); describe("SdkTupleType", () => { it("model with tupled properties", async function () {