From 142dc85d768995b25941c8f03a4c6e84627db26a Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 14 Aug 2024 10:56:54 +0800 Subject: [PATCH 01/10] wip --- .../src/public-utils.ts | 4 +++- packages/typespec-client-generator-core/src/types.ts | 10 ++++++---- .../test/decorators.test.ts | 12 ++++++++---- .../test/package.test.ts | 4 ++-- .../test/public-utils.test.ts | 4 ++-- .../test/types/multipart-types.test.ts | 4 ++-- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index ac0bccf24f..1f7f74b4f6 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -374,7 +374,9 @@ function getContextPath( if (httpOperation.parameters.body) { visited.clear(); result = [{ name: root.name }]; - if (dfsModelProperties(typeToFind, httpOperation.parameters.body.type, "Request")) { + if (dfsModelProperties(typeToFind, httpOperation.parameters.body.type.kind === "Model" + ? getEffectivePayloadType(context, httpOperation.parameters.body.type) + : httpOperation.parameters.body.type, "Request")) { return result; } } diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index d08e2a2de3..27ae73d22d 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -1274,8 +1274,8 @@ function updateMultiPartInfo( : undefined, contentType: httpOperationPart.body.contentTypeProperty ? diagnostics.pipe( - getSdkModelPropertyType(context, httpOperationPart.body.contentTypeProperty, operation) - ) + getSdkModelPropertyType(context, httpOperationPart.body.contentTypeProperty, operation) + ) : undefined, defaultContentTypes: httpOperationPart.body.contentTypes, }; @@ -1556,7 +1556,9 @@ function updateTypesFromOperation( const httpBody = httpOperation.parameters.body; if (httpBody && !isNeverOrVoidType(httpBody.type)) { const sdkType = diagnostics.pipe( - getClientTypeWithDiagnostics(context, httpBody.type, operation) + getClientTypeWithDiagnostics(context, httpBody.type.kind === "Model" + ? getEffectivePayloadType(context, httpBody.type) + : httpBody.type, operation) ); if (generateConvenient) { // Special logic for spread body model: @@ -1573,7 +1575,7 @@ function updateTypesFromOperation( (operation.parameters.properties.get(k) === (httpBody.type as Model).properties.get(k) || operation.parameters.properties.get(k) === - (httpBody.type as Model).properties.get(k)?.sourceProperty) + (httpBody.type as Model).properties.get(k)?.sourceProperty) ) ) { if (!context.spreadModels?.has(httpBody.type)) { diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index e1fb61cf76..9ab8adc2ff 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -3976,11 +3976,11 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(runnerWithVersion.context.sdkPackage.models.length, 2); strictEqual( runnerWithVersion.context.sdkPackage.models[0].name, - "PreviewFunctionalityRequest" + "PreviewModel" ); strictEqual( runnerWithVersion.context.sdkPackage.models[1].name, - "StableFunctionalityRequest" + "StableModel" ); runnerWithVersion = await createSdkTestRunner({ @@ -3998,7 +3998,11 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(runnerWithVersion.context.sdkPackage.models.length, 1); strictEqual( runnerWithVersion.context.sdkPackage.models[0].name, - "StableFunctionalityRequest" + "StableModel" + ); + strictEqual( + runnerWithVersion.context.sdkPackage.models[0].usage, + UsageFlags.Spread | UsageFlags.Json ); }); it("add client", async () => { @@ -4492,7 +4496,7 @@ describe("typespec-client-generator-core: decorators", () => { await runner.compileWithCustomization(mainCode, customizationCode); // runner has python scope, so shouldn't be overridden - ok(!runner.context.sdkPackage.models.find((x) => x.name === "Params")); + ok(runner.context.sdkPackage.models.find((x) => x.name === "Params")); const sdkPackage = runner.context.sdkPackage; const client = sdkPackage.clients[0]; strictEqual(client.methods.length, 1); diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index 01816485bb..34b502af30 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -1793,7 +1793,7 @@ describe("typespec-client-generator-core: package", () => { ); const sdkPackage = runner.context.sdkPackage; const method = getServiceMethodOfClient(sdkPackage); - strictEqual(sdkPackage.models.length, 3); + strictEqual(sdkPackage.models.length, 2); strictEqual(method.name, "create"); const serviceResponses = method.operation.responses; strictEqual(serviceResponses.size, 1); @@ -2148,7 +2148,7 @@ describe("typespec-client-generator-core: package", () => { strictEqual(bodyParameter.type.kind, "model"); strictEqual( bodyParameter.type, - sdkPackage.models.filter((m) => m.name === "UpdateRequest")[0] + sdkPackage.models.filter((m) => m.name === "Widget")[0] ); const headerParams = serviceOperation.parameters.filter( 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 fab7935ead..1fdfda1b24 100644 --- a/packages/typespec-client-generator-core/test/public-utils.test.ts +++ b/packages/typespec-client-generator-core/test/public-utils.test.ts @@ -1641,7 +1641,7 @@ describe("typespec-client-generator-core: public-utils", () => { strictEqual(repeatabilityResult.type.kind, "Union"); const unionEnum = getSdkUnion(runner.context, repeatabilityResult.type); strictEqual(unionEnum.kind, "enum"); - strictEqual(unionEnum.name, "TestRequestRepeatabilityResult"); + strictEqual(unionEnum.name, "RequestParameterWithAnonymousUnionRepeatabilityResult"); // not a defined type in tsp, so no crossLanguageDefinitionId strictEqual( unionEnum.crossLanguageDefinitionId, @@ -1675,7 +1675,7 @@ describe("typespec-client-generator-core: public-utils", () => { strictEqual(stringType.values[1].kind, "enumvalue"); strictEqual(stringType.values[1].value, "rejected"); strictEqual(stringType.valueType.kind, "string"); - strictEqual(stringType.name, "TestRequestRepeatabilityResult"); + strictEqual(stringType.name, "RequestParameterWithAnonymousUnionRepeatabilityResult"); strictEqual(stringType.isGeneratedName, true); strictEqual( stringType.crossLanguageDefinitionId, 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 8bddd65588..f8acc9d444 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 @@ -82,7 +82,7 @@ describe("typespec-client-generator-core: multipart types", () => { ); const models = runner.context.sdkPackage.models; strictEqual(models.length, 2); - const modelA = models.find((x) => x.name === "MultipartOperationRequest"); + const modelA = models.find((x) => x.name === "A"); ok(modelA); strictEqual(modelA.kind, "model"); strictEqual(modelA.isFormDataType, true); @@ -94,7 +94,7 @@ describe("typespec-client-generator-core: multipart types", () => { ok(modelAProp.multipartOptions); strictEqual(modelAProp.multipartOptions.isFilePart, true); - const modelB = models.find((x) => x.name === "NormalOperationRequest"); + const modelB = models.find((x) => x.name === "B"); ok(modelB); strictEqual(modelB.kind, "model"); strictEqual(modelB.isFormDataType, false); From 726dbfa14a80c9f51732c504b617c41029f21091 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 14 Aug 2024 20:42:30 +0800 Subject: [PATCH 02/10] wip --- .../src/http.ts | 28 +++++---- .../src/internal-utils.ts | 49 ++++++++++++++- .../src/public-utils.ts | 12 +++- .../src/types.ts | 62 +++++++++---------- .../test/package.test.ts | 15 ++--- .../test/public-utils.test.ts | 4 +- .../test/types/multipart-types.test.ts | 4 +- 7 files changed, 114 insertions(+), 60 deletions(-) diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 476118455e..53944ecc07 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -1,5 +1,6 @@ import { Diagnostic, + Model, ModelProperty, Operation, Type, @@ -41,11 +42,13 @@ import { import { getAvailableApiVersions, getDocHelper, + getHttpBodySpreadModel, getHttpOperationResponseHeaders, getLocationOfOperation, getTypeDecorators, isAcceptHeader, isContentTypeHeader, + isHttpBodySpread, isNeverOrVoidType, isSubscriptionId, } from "./internal-utils.js"; @@ -149,9 +152,13 @@ function getSdkHttpParameters( } retval.bodyParam = getParamResponse; } else if (!isNeverOrVoidType(tspBody.type)) { - const type = diagnostics.pipe( - getClientTypeWithDiagnostics(context, tspBody.type, httpOperation.operation) - ); + const spread = isHttpBodySpread(tspBody, httpOperation.operation.parameters); + let type: SdkType; + if (spread) { + type = diagnostics.pipe(getClientTypeWithDiagnostics(context, getHttpBodySpreadModel(tspBody.type as Model), httpOperation.operation)); + } else { + type = diagnostics.pipe(getClientTypeWithDiagnostics(context, tspBody.type, httpOperation.operation)); + } const name = camelCase((type as { name: string }).name ?? "body"); retval.bodyParam = { kind: "body", @@ -367,12 +374,12 @@ function getSdkHttpResponseAndExceptions( context: TCGCContext, httpOperation: HttpOperation ): [ - { - responses: Map; - exceptions: Map; - }, - readonly Diagnostic[], -] { + { + responses: Map; + exceptions: Map; + }, + readonly Diagnostic[], + ] { const diagnostics = createDiagnosticCollector(); const responses: Map = new Map(); const exceptions: Map = new Map(); @@ -550,8 +557,7 @@ function findMapping( if ( methodParam.__raw && serviceParam.__raw && - (methodParam.__raw === serviceParam.__raw || - methodParam.__raw === serviceParam.__raw.sourceProperty) + methodParam.__raw.node === serviceParam.__raw.node ) { return methodParam; } diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 156c1fcc5a..60a5cd2e45 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -10,6 +10,7 @@ import { isNeverType, isNullType, isVoidType, + Model, ModelProperty, Namespace, Numeric, @@ -21,7 +22,7 @@ import { Union, Value, } from "@typespec/compiler"; -import { HttpOperation, HttpOperationResponseContent, HttpStatusCodeRange } from "@typespec/http"; +import { HttpOperation, HttpOperationBody, HttpOperationMultipartBody, HttpOperationResponseContent, HttpStatusCodeRange } from "@typespec/http"; import { getAddedOnVersions, getRemovedOnVersions, getVersions } from "@typespec/versioning"; import { DecoratorInfo, @@ -440,8 +441,8 @@ function isOperationBodyType(context: TCGCContext, type: Type, operation?: Opera : undefined; return Boolean( httpBody && - httpBody.type.kind === "Model" && - getEffectivePayloadType(context, httpBody.type) === getEffectivePayloadType(context, type) + httpBody.type.kind === "Model" && + getEffectivePayloadType(context, httpBody.type) === getEffectivePayloadType(context, type) ); } @@ -546,3 +547,45 @@ export function isXmlContentType(contentType: string): boolean { const regex = new RegExp(/^(application|text)\/(.+\+)?xml$/); return regex.test(contentType); } + +/** + * If body is from spread, then it should be an anonymous model. + * Also all model properties should be + * either equal to one of operation parameters (for case spread from model without property with metadata decorator) + * or its source property equal to one of operation parameters (for case spread from model with property with metadata decorator) + * @param httpBody + * @param parameters + * @returns + */ +export function isHttpBodySpread(httpBody: HttpOperationBody | HttpOperationMultipartBody, parameters: Model) { + return httpBody.type.kind === "Model" && + httpBody.type.name === "" && + [...httpBody.type.properties.keys()].every( + (k) => + parameters.properties.has(k) && + (parameters.properties.get(k) === + (httpBody.type as Model).properties.get(k) || + parameters.properties.get(k) === + (httpBody.type as Model).properties.get(k)?.sourceProperty) + ) +} + +/** + * If body is from simple spread, then we use the original model as body model. + * @param type + * @returns + */ +export function getHttpBodySpreadModel(type: Model): Model { + if (type.sourceModels.length === 1 && type.sourceModels[0].usage === "spread") { + const innerModel = type.sourceModels[0].model; + // for case: `op test(...Model):void`; + if (innerModel.name !== "") { + return innerModel; + } + // for case: `op test(@header h: string, @query q: string, ...Model): void`; + if (innerModel.sourceModels.length === 1 && innerModel.sourceModels[0].usage === "spread" && innerModel.sourceModels[0].model.name !== "") { + return innerModel.sourceModels[0].model; + } + } + return type; +} diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index 1f7f74b4f6..78ed6b3f16 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -37,7 +37,9 @@ import { import { TspLiteralType, getClientNamespaceStringHelper, + getHttpBodySpreadModel, getHttpOperationResponseHeaders, + isHttpBodySpread, parseEmitterName, removeVersionsLargerThanExplicitlySpecified, } from "./internal-utils.js"; @@ -374,9 +376,13 @@ function getContextPath( if (httpOperation.parameters.body) { visited.clear(); result = [{ name: root.name }]; - if (dfsModelProperties(typeToFind, httpOperation.parameters.body.type.kind === "Model" - ? getEffectivePayloadType(context, httpOperation.parameters.body.type) - : httpOperation.parameters.body.type, "Request")) { + let bodyType: Type; + if (isHttpBodySpread(httpOperation.parameters.body, httpOperation.operation.parameters)) { + bodyType = getHttpBodySpreadModel(httpOperation.parameters.body.type as Model); + } else { + bodyType = httpOperation.parameters.body.type; + } + if (dfsModelProperties(typeToFind, bodyType, "Request")) { return result; } } diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 27ae73d22d..58e25c48fd 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -84,6 +84,7 @@ import { filterApiVersionsInEnum, getAvailableApiVersions, getDocHelper, + getHttpBodySpreadModel, getHttpOperationResponseHeaders, getLocationOfOperation, getNonNullOptions, @@ -92,6 +93,7 @@ import { getTypeDecorators, intOrFloat, isAzureCoreModel, + isHttpBodySpread, isJsonContentType, isMultipartFormData, isMultipartOperation, @@ -1341,7 +1343,7 @@ export function getSdkModelPropertyType( if ( type.model && httpOperation.parameters.body && - httpOperation.parameters.body.type === type.model + httpOperation.parameters.body.type.node === type.model.node ) { // only add multipartOptions for property of multipart body diagnostics.pipe(updateMultiPartInfo(context, type, result, operation)); @@ -1449,6 +1451,7 @@ function updateModelsMap( interface ModelUsageOptions { seenModelNames?: Set; propagation?: boolean; + skipFirst?: boolean; // this is used to prevent propagation usage from subtype to base type's other subtypes ignoreSubTypeStack?: boolean[]; } @@ -1489,11 +1492,15 @@ function updateUsageOfModel( if (type.kind !== "model" && type.kind !== "enum") return; options.seenModelNames.add(type); - const usageOverride = getUsageOverride(context, type.__raw as any); - if (usageOverride) { - type.usage |= usageOverride | usage; + if (!options.skipFirst) { + const usageOverride = getUsageOverride(context, type.__raw as any); + if (usageOverride) { + type.usage |= usageOverride | usage; + } else { + type.usage |= usage; + } } else { - type.usage |= usage; + options.skipFirst = false; } if (type.kind === "enum") return; @@ -1555,34 +1562,23 @@ function updateTypesFromOperation( } const httpBody = httpOperation.parameters.body; if (httpBody && !isNeverOrVoidType(httpBody.type)) { - const sdkType = diagnostics.pipe( - getClientTypeWithDiagnostics(context, httpBody.type.kind === "Model" - ? getEffectivePayloadType(context, httpBody.type) - : httpBody.type, operation) - ); + const spread = isHttpBodySpread(httpBody, operation.parameters); + let sdkType: SdkType; + if (spread) { + sdkType = diagnostics.pipe(getClientTypeWithDiagnostics(context, getHttpBodySpreadModel(httpBody.type as Model), operation)); + } else { + sdkType = diagnostics.pipe(getClientTypeWithDiagnostics(context, httpBody.type, operation)); + } if (generateConvenient) { - // Special logic for spread body model: - // If body is from spread, then it should be an anonymous model. - // Also all model properties should be - // either equal to one of operation parameters (for case spread from model without property with metadata decorator) - // or its source property equal to one of operation parameters (for case spread from model with property with metadata decorator) - if ( - httpBody.type.kind === "Model" && - httpBody.type.name === "" && - [...httpBody.type.properties.keys()].every( - (k) => - operation.parameters.properties.has(k) && - (operation.parameters.properties.get(k) === - (httpBody.type as Model).properties.get(k) || - operation.parameters.properties.get(k) === - (httpBody.type as Model).properties.get(k)?.sourceProperty) - ) - ) { - if (!context.spreadModels?.has(httpBody.type)) { + if (spread) { + if (!context.spreadModels?.has(httpBody.type as Model)) { context.spreadModels?.set(httpBody.type as Model, sdkType as SdkModelType); } + updateUsageOfModel(context, UsageFlags.Input, sdkType, {skipFirst: true}); + } else { + updateUsageOfModel(context, UsageFlags.Input, sdkType); } - updateUsageOfModel(context, UsageFlags.Input, sdkType); + if (httpBody.contentTypes.some((x) => isJsonContentType(x))) { updateUsageOfModel(context, UsageFlags.Json, sdkType); } @@ -1688,9 +1684,11 @@ function updateAccessOfModel(context: TCGCContext): void { function updateSpreadModelUsageAndAccess(context: TCGCContext): void { for (const sdkType of context.spreadModels?.values() ?? []) { - // if a type has spread usage, then it must be internal - sdkType.access = "internal"; - sdkType.usage = (sdkType.usage & ~UsageFlags.Input) | UsageFlags.Spread; + sdkType.usage |= UsageFlags.Spread; + if ((sdkType.usage & (UsageFlags.Input | UsageFlags.Output)) === 0) { + // if a type has spread usage, but not used in any other operation, then set it to be internal + sdkType.access = "internal"; + } } } diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index 34b502af30..ab39d2e887 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -1990,13 +1990,13 @@ describe("typespec-client-generator-core: package", () => { const bodyParameter = method.operation.bodyParam; ok(bodyParameter); strictEqual(bodyParameter.kind, "body"); - strictEqual(bodyParameter.name, "createRequest"); + strictEqual(bodyParameter.name, "widget"); strictEqual(bodyParameter.onClient, false); strictEqual(bodyParameter.optional, false); strictEqual(bodyParameter.type.kind, "model"); - strictEqual(bodyParameter.type.name, "CreateRequest"); - strictEqual(bodyParameter.type.properties.length, 2); - strictEqual(bodyParameter.correspondingMethodParams.length, 2); + strictEqual(bodyParameter.type.name, "Widget"); + strictEqual(bodyParameter.type.properties.length, 3); + strictEqual(bodyParameter.correspondingMethodParams.length, 3); strictEqual(method.operation.parameters.length, 2); @@ -2167,7 +2167,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, ["id", "weight", "color"]); strictEqual(operationContentTypeParam.correspondingMethodParams[0], methodContentTypeParam); strictEqual(operationAcceptParam.correspondingMethodParams[0], methodAcceptParam); @@ -3473,8 +3473,9 @@ describe("typespec-client-generator-core: package", () => { strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); strictEqual(bodyParameter.type.access, "internal"); - strictEqual(bodyParameter.correspondingMethodParams.length, 1); - deepStrictEqual(bodyParameter.correspondingMethodParams[0], b); + strictEqual(bodyParameter.correspondingMethodParams.length, 2); + deepStrictEqual(bodyParameter.correspondingMethodParams[0], a); + deepStrictEqual(bodyParameter.correspondingMethodParams[1], b); }); it("explicit multiple spread", async () => { 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 1fdfda1b24..711d6eeae0 100644 --- a/packages/typespec-client-generator-core/test/public-utils.test.ts +++ b/packages/typespec-client-generator-core/test/public-utils.test.ts @@ -1645,7 +1645,7 @@ describe("typespec-client-generator-core: public-utils", () => { // not a defined type in tsp, so no crossLanguageDefinitionId strictEqual( unionEnum.crossLanguageDefinitionId, - "test.RequestRepeatabilityResult.anonymous" + "RequestParameterWithAnonymousUnion.repeatabilityResult.anonymous" ); ok(unionEnum.isGeneratedName); }); @@ -1679,7 +1679,7 @@ describe("typespec-client-generator-core: public-utils", () => { strictEqual(stringType.isGeneratedName, true); strictEqual( stringType.crossLanguageDefinitionId, - "test.RequestRepeatabilityResult.anonymous" + "RequestParameterWithAnonymousUnion.repeatabilityResult.anonymous" ); }); 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 f8acc9d444..0dcdc901ae 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 @@ -256,8 +256,8 @@ describe("typespec-client-generator-core: multipart types", () => { const formDataBodyParam = formDataOp.bodyParam; ok(formDataBodyParam); strictEqual(formDataBodyParam.type.kind, "model"); - strictEqual(formDataBodyParam.type.name, "UploadRequest"); - strictEqual(formDataBodyParam.correspondingMethodParams.length, 4); + strictEqual(formDataBodyParam.type.name, "WidgetForm"); + strictEqual(formDataBodyParam.correspondingMethodParams.length, 5); }); it("usage doesn't apply to properties of a form data", async function () { From a86dcf765c513f6bbb08809af26818c759ab3963 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Fri, 16 Aug 2024 14:55:11 +0800 Subject: [PATCH 03/10] finish spread logic --- .../src/internal-utils.ts | 20 ++++--------------- .../src/types.ts | 16 +++++++-------- .../test/package.test.ts | 19 +++++++++--------- .../test/public-utils.test.ts | 8 ++++---- .../test/types/multipart-types.test.ts | 4 ++-- 5 files changed, 27 insertions(+), 40 deletions(-) diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 60a5cd2e45..1e1bf80a4c 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -549,25 +549,13 @@ export function isXmlContentType(contentType: string): boolean { } /** - * If body is from spread, then it should be an anonymous model. - * Also all model properties should be - * either equal to one of operation parameters (for case spread from model without property with metadata decorator) - * or its source property equal to one of operation parameters (for case spread from model with property with metadata decorator) + * If body is from spread, then it does not directly from a model property. * @param httpBody * @param parameters * @returns */ export function isHttpBodySpread(httpBody: HttpOperationBody | HttpOperationMultipartBody, parameters: Model) { - return httpBody.type.kind === "Model" && - httpBody.type.name === "" && - [...httpBody.type.properties.keys()].every( - (k) => - parameters.properties.has(k) && - (parameters.properties.get(k) === - (httpBody.type as Model).properties.get(k) || - parameters.properties.get(k) === - (httpBody.type as Model).properties.get(k)?.sourceProperty) - ) + return httpBody.property === undefined; } /** @@ -579,11 +567,11 @@ export function getHttpBodySpreadModel(type: Model): Model { if (type.sourceModels.length === 1 && type.sourceModels[0].usage === "spread") { const innerModel = type.sourceModels[0].model; // for case: `op test(...Model):void`; - if (innerModel.name !== "") { + if (innerModel.name !== "" && innerModel.properties.size === type.properties.size) { return innerModel; } // for case: `op test(@header h: string, @query q: string, ...Model): void`; - if (innerModel.sourceModels.length === 1 && innerModel.sourceModels[0].usage === "spread" && innerModel.sourceModels[0].model.name !== "") { + if (innerModel.sourceModels.length === 1 && innerModel.sourceModels[0].usage === "spread" && innerModel.sourceModels[0].model.name !== "" && innerModel.sourceModels[0].model.properties.size === type.properties.size) { return innerModel.sourceModels[0].model; } } diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 0e2d0a1ab6..e564802506 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -1345,15 +1345,15 @@ export function getSdkModelPropertyType( isMultipartFileInput: false, flatten: shouldFlattenProperty(context, type), }; - if (operation) { + if (operation && type.model) { const httpOperation = getHttpOperationWithCache(context, operation); - if ( - type.model && - httpOperation.parameters.body && - httpOperation.parameters.body.type.node === type.model.node - ) { - // only add multipartOptions for property of multipart body - diagnostics.pipe(updateMultiPartInfo(context, type, result, operation)); + const httpBody = httpOperation.parameters.body; + if (httpBody) { + const httpBodyType = isHttpBodySpread(httpBody, operation.parameters) ? getHttpBodySpreadModel(httpBody.type as Model): httpBody.type; + if (type.model === httpBodyType){ + // only try to add multipartOptions for property of body + diagnostics.pipe(updateMultiPartInfo(context, type, result, operation)); + } } } return diagnostics.wrap(result); diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index aad85f64db..04f877b87a 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -1089,7 +1089,7 @@ describe("typespec-client-generator-core: package", () => { ); const sdkPackage = runner.context.sdkPackage; const method = getServiceMethodOfClient(sdkPackage); - strictEqual(sdkPackage.models.length, 2); + strictEqual(sdkPackage.models.length, 3); strictEqual(method.name, "create"); const serviceResponses = method.operation.responses; strictEqual(serviceResponses.size, 1); @@ -1286,13 +1286,13 @@ describe("typespec-client-generator-core: package", () => { const bodyParameter = method.operation.bodyParam; ok(bodyParameter); strictEqual(bodyParameter.kind, "body"); - strictEqual(bodyParameter.name, "widget"); + strictEqual(bodyParameter.name, "createRequest"); strictEqual(bodyParameter.onClient, false); strictEqual(bodyParameter.optional, false); strictEqual(bodyParameter.type.kind, "model"); - strictEqual(bodyParameter.type.name, "Widget"); - strictEqual(bodyParameter.type.properties.length, 3); - strictEqual(bodyParameter.correspondingMethodParams.length, 3); + strictEqual(bodyParameter.type.name, "CreateRequest"); + strictEqual(bodyParameter.type.properties.length, 2); + strictEqual(bodyParameter.correspondingMethodParams.length, 2); strictEqual(method.operation.parameters.length, 2); @@ -1444,7 +1444,7 @@ describe("typespec-client-generator-core: package", () => { strictEqual(bodyParameter.type.kind, "model"); strictEqual( bodyParameter.type, - sdkPackage.models.filter((m) => m.name === "Widget")[0] + sdkPackage.models.filter((m) => m.name === "UpdateRequest")[0] ); const headerParams = serviceOperation.parameters.filter( @@ -1463,7 +1463,7 @@ describe("typespec-client-generator-core: package", () => { strictEqual(operationAcceptParam.optional, false); const correspondingMethodParams = bodyParameter.correspondingMethodParams.map((x) => x.name); - deepStrictEqual(correspondingMethodParams, ["id", "weight", "color"]); + deepStrictEqual(correspondingMethodParams, ["weight", "color"]); strictEqual(operationContentTypeParam.correspondingMethodParams[0], methodContentTypeParam); strictEqual(operationAcceptParam.correspondingMethodParams[0], methodAcceptParam); @@ -2769,9 +2769,8 @@ describe("typespec-client-generator-core: package", () => { strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); strictEqual(bodyParameter.type.access, "internal"); - strictEqual(bodyParameter.correspondingMethodParams.length, 2); - deepStrictEqual(bodyParameter.correspondingMethodParams[0], a); - deepStrictEqual(bodyParameter.correspondingMethodParams[1], b); + strictEqual(bodyParameter.correspondingMethodParams.length, 1); + deepStrictEqual(bodyParameter.correspondingMethodParams[0], b); }); it("explicit multiple spread", async () => { 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 3d17a8bd07..6d30073c5e 100644 --- a/packages/typespec-client-generator-core/test/public-utils.test.ts +++ b/packages/typespec-client-generator-core/test/public-utils.test.ts @@ -1639,11 +1639,11 @@ describe("typespec-client-generator-core: public-utils", () => { strictEqual(repeatabilityResult.type.kind, "Union"); const unionEnum = getSdkUnion(runner.context, repeatabilityResult.type); strictEqual(unionEnum.kind, "enum"); - strictEqual(unionEnum.name, "RequestParameterWithAnonymousUnionRepeatabilityResult"); + strictEqual(unionEnum.name, "TestRequestRepeatabilityResult"); // not a defined type in tsp, so no crossLanguageDefinitionId strictEqual( unionEnum.crossLanguageDefinitionId, - "RequestParameterWithAnonymousUnion.repeatabilityResult.anonymous" + "test.RequestRepeatabilityResult.anonymous" ); ok(unionEnum.isGeneratedName); }); @@ -1673,11 +1673,11 @@ describe("typespec-client-generator-core: public-utils", () => { strictEqual(stringType.values[1].kind, "enumvalue"); strictEqual(stringType.values[1].value, "rejected"); strictEqual(stringType.valueType.kind, "string"); - strictEqual(stringType.name, "RequestParameterWithAnonymousUnionRepeatabilityResult"); + strictEqual(stringType.name, "TestRequestRepeatabilityResult"); strictEqual(stringType.isGeneratedName, true); strictEqual( stringType.crossLanguageDefinitionId, - "RequestParameterWithAnonymousUnion.repeatabilityResult.anonymous" + "test.RequestRepeatabilityResult.anonymous" ); }); 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 362f725f81..fb8e8fe68d 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 @@ -255,8 +255,8 @@ describe("typespec-client-generator-core: multipart types", () => { const formDataBodyParam = formDataOp.bodyParam; ok(formDataBodyParam); strictEqual(formDataBodyParam.type.kind, "model"); - strictEqual(formDataBodyParam.type.name, "WidgetForm"); - strictEqual(formDataBodyParam.correspondingMethodParams.length, 5); + strictEqual(formDataBodyParam.type.name, "UploadRequest"); + strictEqual(formDataBodyParam.correspondingMethodParams.length, 4); }); it("usage doesn't apply to properties of a form data", async function () { From 76426c7e2fafc9fd4168e8f1b099c7096113f615 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Fri, 16 Aug 2024 15:00:39 +0800 Subject: [PATCH 04/10] changelog --- .chronus/changes/spread_model-2024-7-16-15-0-33.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/spread_model-2024-7-16-15-0-33.md diff --git a/.chronus/changes/spread_model-2024-7-16-15-0-33.md b/.chronus/changes/spread_model-2024-7-16-15-0-33.md new file mode 100644 index 0000000000..cfb140f668 --- /dev/null +++ b/.chronus/changes/spread_model-2024-7-16-15-0-33.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +use original model for spread if it is from a simple spread \ No newline at end of file From 57bc4eb07dfbd4345b9eaa00a47696ee698bda30 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Fri, 16 Aug 2024 15:09:32 +0800 Subject: [PATCH 05/10] format --- .../src/http.ts | 24 ++++++++----- .../src/internal-utils.ts | 34 +++++++++++++------ .../src/types.ts | 20 +++++++---- .../test/decorators.test.ts | 15 ++------ 4 files changed, 57 insertions(+), 36 deletions(-) diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 6c40c5f58d..f8cbe026f3 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -159,9 +159,17 @@ function getSdkHttpParameters( const spread = isHttpBodySpread(tspBody, httpOperation.operation.parameters); let type: SdkType; if (spread) { - type = diagnostics.pipe(getClientTypeWithDiagnostics(context, getHttpBodySpreadModel(tspBody.type as Model), httpOperation.operation)); + type = diagnostics.pipe( + getClientTypeWithDiagnostics( + context, + getHttpBodySpreadModel(tspBody.type as Model), + httpOperation.operation + ) + ); } else { - type = diagnostics.pipe(getClientTypeWithDiagnostics(context, tspBody.type, httpOperation.operation)); + type = diagnostics.pipe( + getClientTypeWithDiagnostics(context, tspBody.type, httpOperation.operation) + ); } const name = camelCase((type as { name: string }).name ?? "body"); retval.bodyParam = { @@ -392,12 +400,12 @@ function getSdkHttpResponseAndExceptions( context: TCGCContext, httpOperation: HttpOperation ): [ - { - responses: Map; - exceptions: Map; - }, - readonly Diagnostic[], - ] { + { + responses: Map; + exceptions: Map; + }, + readonly Diagnostic[], +] { const diagnostics = createDiagnosticCollector(); const responses: Map = new Map(); const exceptions: Map = new Map(); diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 1e1bf80a4c..e4cf5b3824 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -22,7 +22,13 @@ import { Union, Value, } from "@typespec/compiler"; -import { HttpOperation, HttpOperationBody, HttpOperationMultipartBody, HttpOperationResponseContent, HttpStatusCodeRange } from "@typespec/http"; +import { + HttpOperation, + HttpOperationBody, + HttpOperationMultipartBody, + HttpOperationResponseContent, + HttpStatusCodeRange, +} from "@typespec/http"; import { getAddedOnVersions, getRemovedOnVersions, getVersions } from "@typespec/versioning"; import { DecoratorInfo, @@ -441,8 +447,8 @@ function isOperationBodyType(context: TCGCContext, type: Type, operation?: Opera : undefined; return Boolean( httpBody && - httpBody.type.kind === "Model" && - getEffectivePayloadType(context, httpBody.type) === getEffectivePayloadType(context, type) + httpBody.type.kind === "Model" && + getEffectivePayloadType(context, httpBody.type) === getEffectivePayloadType(context, type) ); } @@ -550,18 +556,21 @@ export function isXmlContentType(contentType: string): boolean { /** * If body is from spread, then it does not directly from a model property. - * @param httpBody - * @param parameters - * @returns + * @param httpBody + * @param parameters + * @returns */ -export function isHttpBodySpread(httpBody: HttpOperationBody | HttpOperationMultipartBody, parameters: Model) { +export function isHttpBodySpread( + httpBody: HttpOperationBody | HttpOperationMultipartBody, + parameters: Model +) { return httpBody.property === undefined; } /** * If body is from simple spread, then we use the original model as body model. - * @param type - * @returns + * @param type + * @returns */ export function getHttpBodySpreadModel(type: Model): Model { if (type.sourceModels.length === 1 && type.sourceModels[0].usage === "spread") { @@ -571,7 +580,12 @@ export function getHttpBodySpreadModel(type: Model): Model { return innerModel; } // for case: `op test(@header h: string, @query q: string, ...Model): void`; - if (innerModel.sourceModels.length === 1 && innerModel.sourceModels[0].usage === "spread" && innerModel.sourceModels[0].model.name !== "" && innerModel.sourceModels[0].model.properties.size === type.properties.size) { + if ( + innerModel.sourceModels.length === 1 && + innerModel.sourceModels[0].usage === "spread" && + innerModel.sourceModels[0].model.name !== "" && + innerModel.sourceModels[0].model.properties.size === type.properties.size + ) { return innerModel.sourceModels[0].model; } } diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index e564802506..bd94f97ceb 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -1283,8 +1283,8 @@ function updateMultiPartInfo( : undefined, contentType: httpOperationPart.body.contentTypeProperty ? diagnostics.pipe( - getSdkModelPropertyType(context, httpOperationPart.body.contentTypeProperty, operation) - ) + getSdkModelPropertyType(context, httpOperationPart.body.contentTypeProperty, operation) + ) : undefined, defaultContentTypes: httpOperationPart.body.contentTypes, }; @@ -1349,8 +1349,10 @@ export function getSdkModelPropertyType( const httpOperation = getHttpOperationWithCache(context, operation); const httpBody = httpOperation.parameters.body; if (httpBody) { - const httpBodyType = isHttpBodySpread(httpBody, operation.parameters) ? getHttpBodySpreadModel(httpBody.type as Model): httpBody.type; - if (type.model === httpBodyType){ + const httpBodyType = isHttpBodySpread(httpBody, operation.parameters) + ? getHttpBodySpreadModel(httpBody.type as Model) + : httpBody.type; + if (type.model === httpBodyType) { // only try to add multipartOptions for property of body diagnostics.pipe(updateMultiPartInfo(context, type, result, operation)); } @@ -1570,7 +1572,13 @@ function updateTypesFromOperation( const spread = isHttpBodySpread(httpBody, operation.parameters); let sdkType: SdkType; if (spread) { - sdkType = diagnostics.pipe(getClientTypeWithDiagnostics(context, getHttpBodySpreadModel(httpBody.type as Model), operation)); + sdkType = diagnostics.pipe( + getClientTypeWithDiagnostics( + context, + getHttpBodySpreadModel(httpBody.type as Model), + operation + ) + ); } else { sdkType = diagnostics.pipe(getClientTypeWithDiagnostics(context, httpBody.type, operation)); } @@ -1579,7 +1587,7 @@ function updateTypesFromOperation( if (!context.spreadModels?.has(httpBody.type as Model)) { context.spreadModels?.set(httpBody.type as Model, sdkType as SdkModelType); } - updateUsageOfModel(context, UsageFlags.Input, sdkType, {skipFirst: true}); + updateUsageOfModel(context, UsageFlags.Input, sdkType, { skipFirst: true }); } else { updateUsageOfModel(context, UsageFlags.Input, sdkType); } diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index d577f05fd8..850acedf0a 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4031,14 +4031,8 @@ describe("typespec-client-generator-core: decorators", () => { "stableFunctionality" ); strictEqual(runnerWithVersion.context.sdkPackage.models.length, 2); - strictEqual( - runnerWithVersion.context.sdkPackage.models[0].name, - "PreviewModel" - ); - strictEqual( - runnerWithVersion.context.sdkPackage.models[1].name, - "StableModel" - ); + strictEqual(runnerWithVersion.context.sdkPackage.models[0].name, "PreviewModel"); + strictEqual(runnerWithVersion.context.sdkPackage.models[1].name, "StableModel"); runnerWithVersion = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python", @@ -4053,10 +4047,7 @@ describe("typespec-client-generator-core: decorators", () => { "stableFunctionality" ); strictEqual(runnerWithVersion.context.sdkPackage.models.length, 1); - strictEqual( - runnerWithVersion.context.sdkPackage.models[0].name, - "StableModel" - ); + strictEqual(runnerWithVersion.context.sdkPackage.models[0].name, "StableModel"); strictEqual( runnerWithVersion.context.sdkPackage.models[0].usage, UsageFlags.Spread | UsageFlags.Json From e696ec8077f1f61e474b8c3667d5ffd9cbb708dd Mon Sep 17 00:00:00 2001 From: tadelesh Date: Mon, 19 Aug 2024 13:51:59 +0800 Subject: [PATCH 06/10] remove useless parameter --- packages/typespec-client-generator-core/src/http.ts | 2 +- packages/typespec-client-generator-core/src/internal-utils.ts | 3 +-- packages/typespec-client-generator-core/src/public-utils.ts | 2 +- packages/typespec-client-generator-core/src/types.ts | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index f8cbe026f3..268a58514b 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -156,7 +156,7 @@ function getSdkHttpParameters( } retval.bodyParam = bodyParam; } else if (!isNeverOrVoidType(tspBody.type)) { - const spread = isHttpBodySpread(tspBody, httpOperation.operation.parameters); + const spread = isHttpBodySpread(tspBody); let type: SdkType; if (spread) { type = diagnostics.pipe( diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index e4cf5b3824..b022147371 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -561,8 +561,7 @@ export function isXmlContentType(contentType: string): boolean { * @returns */ export function isHttpBodySpread( - httpBody: HttpOperationBody | HttpOperationMultipartBody, - parameters: Model + httpBody: HttpOperationBody | HttpOperationMultipartBody ) { return httpBody.property === undefined; } diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index 7ceeb0fc4c..dda9f00a76 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -382,7 +382,7 @@ function getContextPath( visited.clear(); result = [{ name: root.name }]; let bodyType: Type; - if (isHttpBodySpread(httpOperation.parameters.body, httpOperation.operation.parameters)) { + if (isHttpBodySpread(httpOperation.parameters.body)) { bodyType = getHttpBodySpreadModel(httpOperation.parameters.body.type as Model); } else { bodyType = httpOperation.parameters.body.type; diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index bd94f97ceb..27ade5cf79 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -1349,7 +1349,7 @@ export function getSdkModelPropertyType( const httpOperation = getHttpOperationWithCache(context, operation); const httpBody = httpOperation.parameters.body; if (httpBody) { - const httpBodyType = isHttpBodySpread(httpBody, operation.parameters) + const httpBodyType = isHttpBodySpread(httpBody) ? getHttpBodySpreadModel(httpBody.type as Model) : httpBody.type; if (type.model === httpBodyType) { @@ -1569,7 +1569,7 @@ function updateTypesFromOperation( } const httpBody = httpOperation.parameters.body; if (httpBody && !isNeverOrVoidType(httpBody.type)) { - const spread = isHttpBodySpread(httpBody, operation.parameters); + const spread = isHttpBodySpread(httpBody); let sdkType: SdkType; if (spread) { sdkType = diagnostics.pipe( From 18038163cfb3eb683c6598f12b73c366f53e8d40 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Mon, 19 Aug 2024 15:38:49 +0800 Subject: [PATCH 07/10] fix --- packages/typespec-client-generator-core/src/internal-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index b022147371..a2da0e0733 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -574,11 +574,11 @@ export function isHttpBodySpread( export function getHttpBodySpreadModel(type: Model): Model { if (type.sourceModels.length === 1 && type.sourceModels[0].usage === "spread") { const innerModel = type.sourceModels[0].model; - // for case: `op test(...Model):void`; + // for case: `op test(...Model):void;` if (innerModel.name !== "" && innerModel.properties.size === type.properties.size) { return innerModel; } - // for case: `op test(@header h: string, @query q: string, ...Model): void`; + // for case: `op test(@header h: string, @query q: string, ...Model): void;` if ( innerModel.sourceModels.length === 1 && innerModel.sourceModels[0].usage === "spread" && From 43edad9f5c10b29542a2fe844779a3a3ec4ca1d6 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Mon, 19 Aug 2024 15:44:32 +0800 Subject: [PATCH 08/10] format --- packages/typespec-client-generator-core/src/internal-utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index a2da0e0733..66185e2dd8 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -560,9 +560,7 @@ export function isXmlContentType(contentType: string): boolean { * @param parameters * @returns */ -export function isHttpBodySpread( - httpBody: HttpOperationBody | HttpOperationMultipartBody -) { +export function isHttpBodySpread(httpBody: HttpOperationBody | HttpOperationMultipartBody) { return httpBody.property === undefined; } From 93515f16a87630601ff23595645ade716e166d27 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Thu, 22 Aug 2024 13:35:52 +0800 Subject: [PATCH 09/10] refine logic --- .../src/interfaces.ts | 1 - .../typespec-client-generator-core/src/types.ts | 16 +++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index fec31472db..e7ecd615c2 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -37,7 +37,6 @@ export interface TCGCContext { modelsMap?: Map; operationModelsMap?: Map>; generatedNames?: Map; - spreadModels?: Map; httpOperationCache?: Map; unionsMap?: Map; __namespaceToApiVersionParameter: Map; diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index aeee9e53a2..bf2840c19d 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -1318,8 +1318,8 @@ function updateMultiPartInfo( : undefined, contentType: httpOperationPart.body.contentTypeProperty ? diagnostics.pipe( - getSdkModelPropertyType(context, httpOperationPart.body.contentTypeProperty, operation) - ) + getSdkModelPropertyType(context, httpOperationPart.body.contentTypeProperty, operation) + ) : undefined, defaultContentTypes: httpOperationPart.body.contentTypes, }; @@ -1619,9 +1619,7 @@ function updateTypesFromOperation( } if (generateConvenient) { if (spread) { - if (!context.spreadModels?.has(httpBody.type as Model)) { - context.spreadModels?.set(httpBody.type as Model, sdkType as SdkModelType); - } + updateUsageOfModel(context, UsageFlags.Spread, sdkType, { propagation: false }); updateUsageOfModel(context, UsageFlags.Input, sdkType, { skipFirst: true }); } else { updateUsageOfModel(context, UsageFlags.Input, sdkType); @@ -1731,9 +1729,8 @@ function updateAccessOfModel(context: TCGCContext): void { } function updateSpreadModelUsageAndAccess(context: TCGCContext): void { - for (const sdkType of context.spreadModels?.values() ?? []) { - sdkType.usage |= UsageFlags.Spread; - if ((sdkType.usage & (UsageFlags.Input | UsageFlags.Output)) === 0) { + for (const [_, sdkType] of context.modelsMap?.entries() ?? []) { + if ((sdkType.usage & UsageFlags.Spread) > 0 && (sdkType.usage & (UsageFlags.Input | UsageFlags.Output)) === 0) { // if a type has spread usage, but not used in any other operation, then set it to be internal sdkType.access = "internal"; } @@ -1819,9 +1816,6 @@ export function getAllModelsWithDiagnostics( if (context.operationModelsMap === undefined) { context.operationModelsMap = new Map>(); } - if (context.spreadModels === undefined) { - context.spreadModels = new Map(); - } for (const client of listClients(context)) { for (const operation of listOperationsInOperationGroup(context, client)) { // operations on a client From 218f989473bc1174e410fc3799a3fbd9ab811899 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Thu, 22 Aug 2024 13:49:04 +0800 Subject: [PATCH 10/10] rearrange tests and add more spread tests --- .../src/types.ts | 9 +- .../test/decorators.test.ts | 3 + .../test/package.test.ts | 805 ---------------- .../test/packages/spread.test.ts | 901 ++++++++++++++++++ 4 files changed, 910 insertions(+), 808 deletions(-) create mode 100644 packages/typespec-client-generator-core/test/packages/spread.test.ts diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index bf2840c19d..30baf2036e 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -1318,8 +1318,8 @@ function updateMultiPartInfo( : undefined, contentType: httpOperationPart.body.contentTypeProperty ? diagnostics.pipe( - getSdkModelPropertyType(context, httpOperationPart.body.contentTypeProperty, operation) - ) + getSdkModelPropertyType(context, httpOperationPart.body.contentTypeProperty, operation) + ) : undefined, defaultContentTypes: httpOperationPart.body.contentTypes, }; @@ -1730,7 +1730,10 @@ function updateAccessOfModel(context: TCGCContext): void { function updateSpreadModelUsageAndAccess(context: TCGCContext): void { for (const [_, sdkType] of context.modelsMap?.entries() ?? []) { - if ((sdkType.usage & UsageFlags.Spread) > 0 && (sdkType.usage & (UsageFlags.Input | UsageFlags.Output)) === 0) { + if ( + (sdkType.usage & UsageFlags.Spread) > 0 && + (sdkType.usage & (UsageFlags.Input | UsageFlags.Output)) === 0 + ) { // if a type has spread usage, but not used in any other operation, then set it to be internal sdkType.access = "internal"; } diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 850acedf0a..689009bf07 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -4032,7 +4032,9 @@ describe("typespec-client-generator-core: decorators", () => { ); strictEqual(runnerWithVersion.context.sdkPackage.models.length, 2); strictEqual(runnerWithVersion.context.sdkPackage.models[0].name, "PreviewModel"); + strictEqual(runnerWithVersion.context.sdkPackage.models[0].access, "internal"); strictEqual(runnerWithVersion.context.sdkPackage.models[1].name, "StableModel"); + strictEqual(runnerWithVersion.context.sdkPackage.models[1].access, "internal"); runnerWithVersion = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python", @@ -4048,6 +4050,7 @@ describe("typespec-client-generator-core: decorators", () => { ); strictEqual(runnerWithVersion.context.sdkPackage.models.length, 1); strictEqual(runnerWithVersion.context.sdkPackage.models[0].name, "StableModel"); + strictEqual(runnerWithVersion.context.sdkPackage.models[0].access, "internal"); strictEqual( runnerWithVersion.context.sdkPackage.models[0].usage, UsageFlags.Spread | UsageFlags.Json diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index 4e70085e27..50229ea0d4 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -12,9 +12,7 @@ import { SdkHttpOperation, SdkPackage, SdkServiceMethod, - UsageFlags, } from "../src/interfaces.js"; -import { getAllModels } from "../src/types.js"; import { SdkTestRunner, createSdkTestRunner } from "./test-host.js"; describe("typespec-client-generator-core: package", () => { @@ -2105,809 +2103,6 @@ describe("typespec-client-generator-core: package", () => { }); }); - describe("spread", () => { - it("plain model with no decorators", async () => { - await runner.compile(`@server("http://localhost:3000", "endpoint") - @service({}) - namespace My.Service; - - model Input { - key: string; - } - - op myOp(...Input): void; - `); - const sdkPackage = runner.context.sdkPackage; - const method = getServiceMethodOfClient(sdkPackage); - strictEqual(method.name, "myOp"); - strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 2); - - const methodParam = method.parameters.find((x) => x.name === "key"); - ok(methodParam); - strictEqual(methodParam.kind, "method"); - strictEqual(methodParam.optional, false); - strictEqual(methodParam.onClient, false); - strictEqual(methodParam.isApiVersionParam, false); - strictEqual(methodParam.type.kind, "string"); - - const contentTypeParam = method.parameters.find((x) => x.name === "contentType"); - ok(contentTypeParam); - strictEqual(contentTypeParam.clientDefaultValue, undefined); - strictEqual(contentTypeParam.type.kind, "constant"); - strictEqual(contentTypeParam.onClient, false); - - 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, sdkPackage.models[0]); - strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); - strictEqual(bodyParameter.type.access, "internal"); - - const correspondingMethodParams = bodyParameter.correspondingMethodParams; - strictEqual(correspondingMethodParams.length, 1); - strictEqual(correspondingMethodParams[0].name, "key"); - }); - - it("alias with no decorators", async () => { - await runner.compile(`@server("http://localhost:3000", "endpoint") - @service({}) - namespace My.Service; - - alias BodyParameter = { - name: string; - }; - - op myOp(...BodyParameter): void; - `); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.models.length, 1); - - const method = getServiceMethodOfClient(sdkPackage); - strictEqual(method.name, "myOp"); - strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 2); - - const methodParam = method.parameters.find((x) => x.name === "name"); - ok(methodParam); - strictEqual(methodParam.kind, "method"); - strictEqual(methodParam.optional, false); - strictEqual(methodParam.onClient, false); - strictEqual(methodParam.isApiVersionParam, false); - strictEqual(methodParam.type.kind, "string"); - - 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, 1); - strictEqual(bodyParameter.type.properties[0].name, "name"); - - const correspondingMethodParams = bodyParameter.correspondingMethodParams; - strictEqual(correspondingMethodParams.length, 1); - strictEqual(bodyParameter.type.properties[0].name, correspondingMethodParams[0].name); - }); - - it("rest template spreading of multiple models", 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.sdkPackage; - strictEqual(sdkPackage.models.length, 4); - deepStrictEqual( - sdkPackage.models.map((x) => x.name).sort(), - ["CheckupCollectionWithNextLink", "Checkup", "PetStoreError", "CheckupUpdate"].sort() - ); - const client = sdkPackage.clients[0].methods.find((x) => x.kind === "clientaccessor") - ?.response as SdkClientType; - const createOrUpdate = client.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("multi layer template with discriminated model spread", async () => { - const runnerWithCore = await createSdkTestRunner({ - librariesToAdd: [AzureCoreTestLibrary], - autoUsings: ["Azure.Core", "Azure.Core.Traits"], - emitterName: "@azure-tools/typespec-java", - }); - await runnerWithCore.compile(` - @versioned(MyVersions) - @server("http://localhost:3000", "endpoint") - @useAuth(ApiKeyAuth) - @service({name: "Service"}) - namespace My.Service; - - alias ServiceTraits = NoRepeatableRequests & - NoConditionalRequests & - NoClientRequestId; - - alias Operations = Azure.Core.ResourceOperations; - - @doc("The version of the API.") - enum MyVersions { - @doc("The version 2022-12-01-preview.") - @useDependency(Versions.v1_0_Preview_2) - v2022_12_01_preview: "2022-12-01-preview", - } - - @discriminator("kind") - @resource("dataConnections") - model DataConnection { - id?: string; - - @key("dataConnectionName") - @visibility("read") - name: string; - - @visibility("read") - createdDate?: utcDateTime; - - frequencyOffset?: int32; - } - - @discriminator("kind") - model DataConnectionData { - name?: string; - frequencyOffset?: int32; - } - - interface DataConnections { - - getDataConnection is Operations.ResourceRead; - - @createsOrReplacesResource(DataConnection) - @put - createOrReplaceDataConnection is Foundations.ResourceOperation< - DataConnection, - DataConnectionData, - DataConnection - >; - - deleteDataConnection is Operations.ResourceDelete; - } - `); - const sdkPackage = runnerWithCore.context.sdkPackage; - strictEqual(sdkPackage.models.length, 2); - - const client = sdkPackage.clients[0].methods.find((x) => x.kind === "clientaccessor") - ?.response as SdkClientType; - - const createOrReplace = client.methods[1]; - strictEqual(createOrReplace.kind, "basic"); - strictEqual(createOrReplace.name, "createOrReplaceDataConnection"); - strictEqual(createOrReplace.parameters.length, 6); - ok( - createOrReplace.parameters.find( - (x) => x.name === "dataConnectionName" && x.type.kind === "string" - ) - ); - ok(createOrReplace.parameters.find((x) => x.name === "name" && x.type.kind === "string")); - ok( - createOrReplace.parameters.find( - (x) => x.name === "frequencyOffset" && x.type.kind === "int32" - ) - ); - ok(createOrReplace.parameters.find((x) => x.name === "contentType")); - ok(createOrReplace.parameters.find((x) => x.name === "accept")); - ok(createOrReplace.parameters.find((x) => x.isApiVersionParam && x.onClient)); - - const opParams = createOrReplace.operation.parameters; - strictEqual(opParams.length, 4); - ok(opParams.find((x) => x.isApiVersionParam === true && x.kind === "query")); - ok(opParams.find((x) => x.kind === "path" && x.serializedName === "dataConnectionName")); - ok(opParams.find((x) => x.kind === "header" && x.serializedName === "Content-Type")); - ok(opParams.find((x) => x.kind === "header" && x.serializedName === "Accept")); - strictEqual(createOrReplace.operation.bodyParam?.type.kind, "model"); - strictEqual( - createOrReplace.operation.bodyParam?.type.name, - "CreateOrReplaceDataConnectionRequest" - ); - deepStrictEqual( - createOrReplace.operation.bodyParam.correspondingMethodParams[0], - createOrReplace.parameters[2] - ); - deepStrictEqual( - createOrReplace.operation.bodyParam.correspondingMethodParams[1], - createOrReplace.parameters[3] - ); - strictEqual(createOrReplace.operation.responses.size, 1); - const response200 = createOrReplace.operation.responses.get(200); - ok(response200); - ok(response200.type); - strictEqual(response200.type.kind, "model"); - strictEqual(response200.type.name, "DataConnection"); - }); - - it("model with @body decorator", async () => { - await runner.compileWithBuiltInService(` - model Shelf { - name: string; - theme?: string; - } - model CreateShelfRequest { - @body - body: Shelf; - } - op createShelf(...CreateShelfRequest): Shelf; - `); - const method = getServiceMethodOfClient(runner.context.sdkPackage); - const models = runner.context.sdkPackage.models; - strictEqual(models.length, 1); - const shelfModel = models.find((x) => x.name === "Shelf"); - ok(shelfModel); - strictEqual(method.parameters.length, 3); - const shelfParameter = method.parameters[0]; - strictEqual(shelfParameter.kind, "method"); - strictEqual(shelfParameter.name, "body"); - strictEqual(shelfParameter.optional, false); - strictEqual(shelfParameter.isGeneratedName, false); - deepStrictEqual(shelfParameter.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[0], shelfParameter); - }); - it("formdata model without body decorator in spread model", async () => { - await runner.compileWithBuiltInService(` - - model DocumentTranslateContent { - @header contentType: "multipart/form-data"; - document: bytes; - } - alias Intersected = DocumentTranslateContent & {}; - op test(...Intersected): void; - `); - const method = getServiceMethodOfClient(runner.context.sdkPackage); - const documentMethodParam = method.parameters.find((x) => x.name === "document"); - ok(documentMethodParam); - strictEqual(documentMethodParam.kind, "method"); - const op = method.operation; - ok(op.bodyParam); - strictEqual(op.bodyParam.kind, "body"); - strictEqual(op.bodyParam.name, "testRequest"); - deepStrictEqual(op.bodyParam.correspondingMethodParams, [documentMethodParam]); - - const anonymousModel = runner.context.sdkPackage.models[0]; - strictEqual(anonymousModel.properties.length, 1); - strictEqual(anonymousModel.properties[0].kind, "property"); - strictEqual(anonymousModel.properties[0].isMultipartFileInput, true); - ok(anonymousModel.properties[0].multipartOptions); - strictEqual(anonymousModel.properties[0].multipartOptions.isFilePart, true); - strictEqual(anonymousModel.properties[0].multipartOptions.isMulti, false); - }); - - it("anonymous model with @body should not be spread", async () => { - await runner.compileWithBuiltInService(` - op test(@body body: {prop: string}): void; - `); - const method = getServiceMethodOfClient(runner.context.sdkPackage); - const models = runner.context.sdkPackage.models; - strictEqual(models.length, 1); - const model = models.find((x) => x.name === "TestRequest"); - ok(model); - strictEqual(model.usage, UsageFlags.Input | UsageFlags.Json); - - strictEqual(method.parameters.length, 2); - const param = method.parameters[0]; - strictEqual(param.kind, "method"); - strictEqual(param.name, "body"); - strictEqual(param.optional, false); - strictEqual(param.isGeneratedName, false); - deepStrictEqual(param.type, model); - const contentTypeMethoParam = method.parameters.find((x) => x.name === "contentType"); - ok(contentTypeMethoParam); - - const op = method.operation; - strictEqual(op.parameters.length, 1); - ok( - op.parameters.find( - (x) => - x.kind === "header" && - x.serializedName === "Content-Type" && - x.correspondingMethodParams[0] === contentTypeMethoParam - ) - ); - - 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, model); - deepStrictEqual(bodyParam.correspondingMethodParams[0].type, model); - }); - - it("anonymous model from spread with @bodyRoot should not be spread", async () => { - await runner.compileWithBuiltInService(` - model Test { - prop: string; - } - op test(@bodyRoot body: {...Test}): void; - `); - const method = getServiceMethodOfClient(runner.context.sdkPackage); - const models = runner.context.sdkPackage.models; - strictEqual(models.length, 1); - const model = models.find((x) => x.name === "TestRequest"); - ok(model); - strictEqual(model.usage, UsageFlags.Input | UsageFlags.Json); - - strictEqual(method.parameters.length, 2); - const param = method.parameters[0]; - strictEqual(param.kind, "method"); - strictEqual(param.name, "body"); - strictEqual(param.optional, false); - strictEqual(param.isGeneratedName, false); - deepStrictEqual(param.type, model); - const contentTypeMethoParam = method.parameters.find((x) => x.name === "contentType"); - ok(contentTypeMethoParam); - - const op = method.operation; - strictEqual(op.parameters.length, 1); - ok( - op.parameters.find( - (x) => - x.kind === "header" && - x.serializedName === "Content-Type" && - x.correspondingMethodParams[0] === contentTypeMethoParam - ) - ); - - 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, model); - deepStrictEqual(bodyParam.correspondingMethodParams[0].type, model); - }); - - it("implicit spread", async () => { - await runner.compile(`@server("http://localhost:3000", "endpoint") - @service({}) - namespace My.Service; - op myOp(a: string, b: string): void; - `); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.models.length, 1); - - const method = getServiceMethodOfClient(sdkPackage); - strictEqual(method.name, "myOp"); - strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 3); - - const a = method.parameters.find((x) => x.name === "a"); - ok(a); - strictEqual(a.kind, "method"); - strictEqual(a.optional, false); - strictEqual(a.onClient, false); - strictEqual(a.isApiVersionParam, false); - strictEqual(a.type.kind, "string"); - - const b = method.parameters.find((x) => x.name === "b"); - ok(b); - strictEqual(b.kind, "method"); - strictEqual(b.optional, false); - strictEqual(b.onClient, false); - strictEqual(b.isApiVersionParam, false); - strictEqual(b.type.kind, "string"); - - const serviceOperation = method.operation; - const bodyParameter = serviceOperation.bodyParam; - ok(bodyParameter); - - strictEqual(bodyParameter.kind, "body"); - strictEqual(bodyParameter.onClient, false); - strictEqual(bodyParameter.optional, false); - strictEqual(bodyParameter.type, sdkPackage.models[0]); - strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); - strictEqual(bodyParameter.type.access, "internal"); - - strictEqual(bodyParameter.correspondingMethodParams.length, 2); - deepStrictEqual(bodyParameter.correspondingMethodParams[0], a); - deepStrictEqual(bodyParameter.correspondingMethodParams[1], b); - }); - - it("implicit spread with metadata", async () => { - await runner.compile(`@server("http://localhost:3000", "endpoint") - @service({}) - namespace My.Service; - op myOp(@header a: string, b: string): void; - `); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.models.length, 1); - - const method = getServiceMethodOfClient(sdkPackage); - strictEqual(method.name, "myOp"); - strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 3); - - const a = method.parameters.find((x) => x.name === "a"); - ok(a); - strictEqual(a.kind, "method"); - strictEqual(a.optional, false); - strictEqual(a.onClient, false); - strictEqual(a.isApiVersionParam, false); - strictEqual(a.type.kind, "string"); - - const b = method.parameters.find((x) => x.name === "b"); - ok(b); - strictEqual(b.kind, "method"); - strictEqual(b.optional, false); - strictEqual(b.onClient, false); - strictEqual(b.isApiVersionParam, false); - strictEqual(b.type.kind, "string"); - - const serviceOperation = method.operation; - const headerParameter = serviceOperation.parameters.find((p) => (p.name = "a")); - ok(headerParameter); - strictEqual(headerParameter.kind, "header"); - strictEqual(headerParameter.onClient, false); - strictEqual(headerParameter.optional, false); - strictEqual(headerParameter.type.kind, "string"); - - strictEqual(headerParameter.correspondingMethodParams.length, 1); - deepStrictEqual(headerParameter.correspondingMethodParams[0], a); - - const bodyParameter = serviceOperation.bodyParam; - ok(bodyParameter); - - strictEqual(bodyParameter.kind, "body"); - strictEqual(bodyParameter.onClient, false); - strictEqual(bodyParameter.optional, false); - strictEqual(bodyParameter.type, sdkPackage.models[0]); - strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); - strictEqual(bodyParameter.type.access, "internal"); - - strictEqual(bodyParameter.correspondingMethodParams.length, 1); - deepStrictEqual(bodyParameter.correspondingMethodParams[0], b); - }); - - it("explicit spread", async () => { - await runner.compile(`@server("http://localhost:3000", "endpoint") - @service({}) - namespace My.Service; - model Test { - a: string; - b: string; - } - op myOp(...Test): void; - `); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.models.length, 1); - - const method = getServiceMethodOfClient(sdkPackage); - strictEqual(method.name, "myOp"); - strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 3); - - const a = method.parameters.find((x) => x.name === "a"); - ok(a); - strictEqual(a.kind, "method"); - strictEqual(a.optional, false); - strictEqual(a.onClient, false); - strictEqual(a.isApiVersionParam, false); - strictEqual(a.type.kind, "string"); - - const b = method.parameters.find((x) => x.name === "b"); - ok(b); - strictEqual(b.kind, "method"); - strictEqual(b.optional, false); - strictEqual(b.onClient, false); - strictEqual(b.isApiVersionParam, false); - strictEqual(b.type.kind, "string"); - - const serviceOperation = method.operation; - const bodyParameter = serviceOperation.bodyParam; - ok(bodyParameter); - - strictEqual(bodyParameter.kind, "body"); - strictEqual(bodyParameter.onClient, false); - strictEqual(bodyParameter.optional, false); - strictEqual(bodyParameter.type, sdkPackage.models[0]); - strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); - strictEqual(bodyParameter.type.access, "internal"); - - strictEqual(bodyParameter.correspondingMethodParams.length, 2); - deepStrictEqual(bodyParameter.correspondingMethodParams[0], a); - deepStrictEqual(bodyParameter.correspondingMethodParams[1], b); - }); - - it("explicit spread with metadata", async () => { - await runner.compile(`@server("http://localhost:3000", "endpoint") - @service({}) - namespace My.Service; - model Test { - @header - a: string; - b: string; - } - op myOp(...Test): void; - `); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.models.length, 1); - - const method = getServiceMethodOfClient(sdkPackage); - strictEqual(method.name, "myOp"); - strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 3); - - const a = method.parameters.find((x) => x.name === "a"); - ok(a); - strictEqual(a.kind, "method"); - strictEqual(a.optional, false); - strictEqual(a.onClient, false); - strictEqual(a.isApiVersionParam, false); - strictEqual(a.type.kind, "string"); - - const b = method.parameters.find((x) => x.name === "b"); - ok(b); - strictEqual(b.kind, "method"); - strictEqual(b.optional, false); - strictEqual(b.onClient, false); - strictEqual(b.isApiVersionParam, false); - strictEqual(b.type.kind, "string"); - - const serviceOperation = method.operation; - const headerParameter = serviceOperation.parameters.find((p) => (p.name = "a")); - ok(headerParameter); - strictEqual(headerParameter.kind, "header"); - strictEqual(headerParameter.onClient, false); - strictEqual(headerParameter.optional, false); - strictEqual(headerParameter.type.kind, "string"); - - strictEqual(headerParameter.correspondingMethodParams.length, 1); - deepStrictEqual(headerParameter.correspondingMethodParams[0], a); - - const bodyParameter = serviceOperation.bodyParam; - ok(bodyParameter); - - strictEqual(bodyParameter.kind, "body"); - strictEqual(bodyParameter.onClient, false); - strictEqual(bodyParameter.optional, false); - strictEqual(bodyParameter.type, sdkPackage.models[0]); - strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); - strictEqual(bodyParameter.type.access, "internal"); - - strictEqual(bodyParameter.correspondingMethodParams.length, 1); - deepStrictEqual(bodyParameter.correspondingMethodParams[0], b); - }); - - it("explicit multiple spread", async () => { - await runner.compile(`@server("http://localhost:3000", "endpoint") - @service({}) - namespace My.Service; - model Test1 { - a: string; - - } - - model Test2 { - b: string; - } - op myOp(...Test1, ...Test2): void; - `); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.models.length, 1); - - const method = getServiceMethodOfClient(sdkPackage); - strictEqual(method.name, "myOp"); - strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 3); - - const a = method.parameters.find((x) => x.name === "a"); - ok(a); - strictEqual(a.kind, "method"); - strictEqual(a.optional, false); - strictEqual(a.onClient, false); - strictEqual(a.isApiVersionParam, false); - strictEqual(a.type.kind, "string"); - - const b = method.parameters.find((x) => x.name === "b"); - ok(b); - strictEqual(b.kind, "method"); - strictEqual(b.optional, false); - strictEqual(b.onClient, false); - strictEqual(b.isApiVersionParam, false); - strictEqual(b.type.kind, "string"); - - const serviceOperation = method.operation; - const bodyParameter = serviceOperation.bodyParam; - ok(bodyParameter); - - strictEqual(bodyParameter.kind, "body"); - strictEqual(bodyParameter.onClient, false); - strictEqual(bodyParameter.optional, false); - strictEqual(bodyParameter.type, sdkPackage.models[0]); - strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); - strictEqual(bodyParameter.type.access, "internal"); - - strictEqual(bodyParameter.correspondingMethodParams.length, 2); - deepStrictEqual(bodyParameter.correspondingMethodParams[0], a); - deepStrictEqual(bodyParameter.correspondingMethodParams[1], b); - }); - - it("explicit multiple spread with metadata", async () => { - await runner.compile(`@server("http://localhost:3000", "endpoint") - @service({}) - namespace My.Service; - model Test1 { - @header - a: string; - } - model Test2 { - b: string; - } - op myOp(...Test1, ...Test2): void; - `); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.models.length, 1); - - const method = getServiceMethodOfClient(sdkPackage); - strictEqual(method.name, "myOp"); - strictEqual(method.kind, "basic"); - strictEqual(method.parameters.length, 3); - - const a = method.parameters.find((x) => x.name === "a"); - ok(a); - strictEqual(a.kind, "method"); - strictEqual(a.optional, false); - strictEqual(a.onClient, false); - strictEqual(a.isApiVersionParam, false); - strictEqual(a.type.kind, "string"); - - const b = method.parameters.find((x) => x.name === "b"); - ok(b); - strictEqual(b.kind, "method"); - strictEqual(b.optional, false); - strictEqual(b.onClient, false); - strictEqual(b.isApiVersionParam, false); - strictEqual(b.type.kind, "string"); - - const serviceOperation = method.operation; - const headerParameter = serviceOperation.parameters.find((p) => (p.name = "a")); - ok(headerParameter); - strictEqual(headerParameter.kind, "header"); - strictEqual(headerParameter.onClient, false); - strictEqual(headerParameter.optional, false); - strictEqual(headerParameter.type.kind, "string"); - - strictEqual(headerParameter.correspondingMethodParams.length, 1); - deepStrictEqual(headerParameter.correspondingMethodParams[0], a); - - const bodyParameter = serviceOperation.bodyParam; - ok(bodyParameter); - - strictEqual(bodyParameter.kind, "body"); - strictEqual(bodyParameter.onClient, false); - strictEqual(bodyParameter.optional, false); - strictEqual(bodyParameter.type, sdkPackage.models[0]); - strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); - strictEqual(bodyParameter.type.access, "internal"); - - strictEqual(bodyParameter.correspondingMethodParams.length, 1); - deepStrictEqual(bodyParameter.correspondingMethodParams[0], b); - }); - - it("spread idempotent", async () => { - await runner.compile(`@server("http://localhost:3000", "endpoint") - @service({}) - namespace My.Service; - alias FooAlias = { - @path id: string; - @doc("name of the Foo") - name: string; - }; - op test(...FooAlias): void; - `); - const sdkPackage = runner.context.sdkPackage; - strictEqual(sdkPackage.models.length, 1); - getAllModels(runner.context); - - strictEqual(sdkPackage.models[0].name, "TestRequest"); - strictEqual(sdkPackage.models[0].usage, UsageFlags.Spread | UsageFlags.Json); - }); - }); describe("versioning", () => { it("define own api version param", async () => { await runner.compileWithBuiltInService(` diff --git a/packages/typespec-client-generator-core/test/packages/spread.test.ts b/packages/typespec-client-generator-core/test/packages/spread.test.ts new file mode 100644 index 0000000000..b4069b32f3 --- /dev/null +++ b/packages/typespec-client-generator-core/test/packages/spread.test.ts @@ -0,0 +1,901 @@ +import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing"; +import { deepStrictEqual, ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { SdkClientType, SdkHttpOperation, UsageFlags } from "../../src/interfaces.js"; +import { getAllModels } from "../../src/types.js"; +import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; +import { getServiceMethodOfClient } from "./utils.js"; + +describe("typespec-client-generator-core: spread", () => { + let runner: SdkTestRunner; + + beforeEach(async () => { + runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python" }); + }); + + it("plain model with no decorators", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + + model Input { + key: string; + } + + op myOp(...Input): void; + `); + const sdkPackage = runner.context.sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "myOp"); + strictEqual(method.kind, "basic"); + strictEqual(method.parameters.length, 2); + + const methodParam = method.parameters.find((x) => x.name === "key"); + ok(methodParam); + strictEqual(methodParam.kind, "method"); + strictEqual(methodParam.optional, false); + strictEqual(methodParam.onClient, false); + strictEqual(methodParam.isApiVersionParam, false); + strictEqual(methodParam.type.kind, "string"); + + const contentTypeParam = method.parameters.find((x) => x.name === "contentType"); + ok(contentTypeParam); + strictEqual(contentTypeParam.clientDefaultValue, undefined); + strictEqual(contentTypeParam.type.kind, "constant"); + strictEqual(contentTypeParam.onClient, false); + + 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, sdkPackage.models[0]); + strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); + strictEqual(bodyParameter.type.access, "internal"); + + const correspondingMethodParams = bodyParameter.correspondingMethodParams; + strictEqual(correspondingMethodParams.length, 1); + strictEqual(correspondingMethodParams[0].name, "key"); + }); + + it("alias with no decorators", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + + alias BodyParameter = { + name: string; + }; + + op myOp(...BodyParameter): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "myOp"); + strictEqual(method.kind, "basic"); + strictEqual(method.parameters.length, 2); + + const methodParam = method.parameters.find((x) => x.name === "name"); + ok(methodParam); + strictEqual(methodParam.kind, "method"); + strictEqual(methodParam.optional, false); + strictEqual(methodParam.onClient, false); + strictEqual(methodParam.isApiVersionParam, false); + strictEqual(methodParam.type.kind, "string"); + + 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, 1); + strictEqual(bodyParameter.type.properties[0].name, "name"); + + const correspondingMethodParams = bodyParameter.correspondingMethodParams; + strictEqual(correspondingMethodParams.length, 1); + strictEqual(bodyParameter.type.properties[0].name, correspondingMethodParams[0].name); + }); + + it("rest template spreading of multiple models", 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.sdkPackage; + strictEqual(sdkPackage.models.length, 4); + deepStrictEqual( + sdkPackage.models.map((x) => x.name).sort(), + ["CheckupCollectionWithNextLink", "Checkup", "PetStoreError", "CheckupUpdate"].sort() + ); + const client = sdkPackage.clients[0].methods.find((x) => x.kind === "clientaccessor") + ?.response as SdkClientType; + const createOrUpdate = client.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("multi layer template with discriminated model spread", async () => { + const runnerWithCore = await createSdkTestRunner({ + librariesToAdd: [AzureCoreTestLibrary], + autoUsings: ["Azure.Core", "Azure.Core.Traits"], + emitterName: "@azure-tools/typespec-java", + }); + await runnerWithCore.compile(` + @versioned(MyVersions) + @server("http://localhost:3000", "endpoint") + @useAuth(ApiKeyAuth) + @service({name: "Service"}) + namespace My.Service; + + alias ServiceTraits = NoRepeatableRequests & + NoConditionalRequests & + NoClientRequestId; + + alias Operations = Azure.Core.ResourceOperations; + + @doc("The version of the API.") + enum MyVersions { + @doc("The version 2022-12-01-preview.") + @useDependency(Versions.v1_0_Preview_2) + v2022_12_01_preview: "2022-12-01-preview", + } + + @discriminator("kind") + @resource("dataConnections") + model DataConnection { + id?: string; + + @key("dataConnectionName") + @visibility("read") + name: string; + + @visibility("read") + createdDate?: utcDateTime; + + frequencyOffset?: int32; + } + + @discriminator("kind") + model DataConnectionData { + name?: string; + frequencyOffset?: int32; + } + + interface DataConnections { + + getDataConnection is Operations.ResourceRead; + + @createsOrReplacesResource(DataConnection) + @put + createOrReplaceDataConnection is Foundations.ResourceOperation< + DataConnection, + DataConnectionData, + DataConnection + >; + + deleteDataConnection is Operations.ResourceDelete; + } + `); + const sdkPackage = runnerWithCore.context.sdkPackage; + strictEqual(sdkPackage.models.length, 2); + + const client = sdkPackage.clients[0].methods.find((x) => x.kind === "clientaccessor") + ?.response as SdkClientType; + + const createOrReplace = client.methods[1]; + strictEqual(createOrReplace.kind, "basic"); + strictEqual(createOrReplace.name, "createOrReplaceDataConnection"); + strictEqual(createOrReplace.parameters.length, 6); + ok( + createOrReplace.parameters.find( + (x) => x.name === "dataConnectionName" && x.type.kind === "string" + ) + ); + ok(createOrReplace.parameters.find((x) => x.name === "name" && x.type.kind === "string")); + ok( + createOrReplace.parameters.find( + (x) => x.name === "frequencyOffset" && x.type.kind === "int32" + ) + ); + ok(createOrReplace.parameters.find((x) => x.name === "contentType")); + ok(createOrReplace.parameters.find((x) => x.name === "accept")); + ok(createOrReplace.parameters.find((x) => x.isApiVersionParam && x.onClient)); + + const opParams = createOrReplace.operation.parameters; + strictEqual(opParams.length, 4); + ok(opParams.find((x) => x.isApiVersionParam === true && x.kind === "query")); + ok(opParams.find((x) => x.kind === "path" && x.serializedName === "dataConnectionName")); + ok(opParams.find((x) => x.kind === "header" && x.serializedName === "Content-Type")); + ok(opParams.find((x) => x.kind === "header" && x.serializedName === "Accept")); + strictEqual(createOrReplace.operation.bodyParam?.type.kind, "model"); + strictEqual( + createOrReplace.operation.bodyParam?.type.name, + "CreateOrReplaceDataConnectionRequest" + ); + deepStrictEqual( + createOrReplace.operation.bodyParam.correspondingMethodParams[0], + createOrReplace.parameters[2] + ); + deepStrictEqual( + createOrReplace.operation.bodyParam.correspondingMethodParams[1], + createOrReplace.parameters[3] + ); + strictEqual(createOrReplace.operation.responses.size, 1); + const response200 = createOrReplace.operation.responses.get(200); + ok(response200); + ok(response200.type); + strictEqual(response200.type.kind, "model"); + strictEqual(response200.type.name, "DataConnection"); + }); + + it("model with @body decorator", async () => { + await runner.compileWithBuiltInService(` + model Shelf { + name: string; + theme?: string; + } + model CreateShelfRequest { + @body + body: Shelf; + } + op createShelf(...CreateShelfRequest): Shelf; + `); + const method = getServiceMethodOfClient(runner.context.sdkPackage); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 1); + const shelfModel = models.find((x) => x.name === "Shelf"); + ok(shelfModel); + strictEqual(method.parameters.length, 3); + const shelfParameter = method.parameters[0]; + strictEqual(shelfParameter.kind, "method"); + strictEqual(shelfParameter.name, "body"); + strictEqual(shelfParameter.optional, false); + strictEqual(shelfParameter.isGeneratedName, false); + deepStrictEqual(shelfParameter.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[0], shelfParameter); + }); + it("formdata model without body decorator in spread model", async () => { + await runner.compileWithBuiltInService(` + + model DocumentTranslateContent { + @header contentType: "multipart/form-data"; + document: bytes; + } + alias Intersected = DocumentTranslateContent & {}; + op test(...Intersected): void; + `); + const method = getServiceMethodOfClient(runner.context.sdkPackage); + const documentMethodParam = method.parameters.find((x) => x.name === "document"); + ok(documentMethodParam); + strictEqual(documentMethodParam.kind, "method"); + const op = method.operation; + ok(op.bodyParam); + strictEqual(op.bodyParam.kind, "body"); + strictEqual(op.bodyParam.name, "testRequest"); + deepStrictEqual(op.bodyParam.correspondingMethodParams, [documentMethodParam]); + + const anonymousModel = runner.context.sdkPackage.models[0]; + strictEqual(anonymousModel.properties.length, 1); + strictEqual(anonymousModel.properties[0].kind, "property"); + strictEqual(anonymousModel.properties[0].isMultipartFileInput, true); + ok(anonymousModel.properties[0].multipartOptions); + strictEqual(anonymousModel.properties[0].multipartOptions.isFilePart, true); + strictEqual(anonymousModel.properties[0].multipartOptions.isMulti, false); + }); + + it("anonymous model with @body should not be spread", async () => { + await runner.compileWithBuiltInService(` + op test(@body body: {prop: string}): void; + `); + const method = getServiceMethodOfClient(runner.context.sdkPackage); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 1); + const model = models.find((x) => x.name === "TestRequest"); + ok(model); + strictEqual(model.usage, UsageFlags.Input | UsageFlags.Json); + + strictEqual(method.parameters.length, 2); + const param = method.parameters[0]; + strictEqual(param.kind, "method"); + strictEqual(param.name, "body"); + strictEqual(param.optional, false); + strictEqual(param.isGeneratedName, false); + deepStrictEqual(param.type, model); + const contentTypeMethoParam = method.parameters.find((x) => x.name === "contentType"); + ok(contentTypeMethoParam); + + const op = method.operation; + strictEqual(op.parameters.length, 1); + ok( + op.parameters.find( + (x) => + x.kind === "header" && + x.serializedName === "Content-Type" && + x.correspondingMethodParams[0] === contentTypeMethoParam + ) + ); + + 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, model); + deepStrictEqual(bodyParam.correspondingMethodParams[0].type, model); + }); + + it("anonymous model from spread with @bodyRoot should not be spread", async () => { + await runner.compileWithBuiltInService(` + model Test { + prop: string; + } + op test(@bodyRoot body: {...Test}): void; + `); + const method = getServiceMethodOfClient(runner.context.sdkPackage); + const models = runner.context.sdkPackage.models; + strictEqual(models.length, 1); + const model = models.find((x) => x.name === "TestRequest"); + ok(model); + strictEqual(model.usage, UsageFlags.Input | UsageFlags.Json); + + strictEqual(method.parameters.length, 2); + const param = method.parameters[0]; + strictEqual(param.kind, "method"); + strictEqual(param.name, "body"); + strictEqual(param.optional, false); + strictEqual(param.isGeneratedName, false); + deepStrictEqual(param.type, model); + const contentTypeMethoParam = method.parameters.find((x) => x.name === "contentType"); + ok(contentTypeMethoParam); + + const op = method.operation; + strictEqual(op.parameters.length, 1); + ok( + op.parameters.find( + (x) => + x.kind === "header" && + x.serializedName === "Content-Type" && + x.correspondingMethodParams[0] === contentTypeMethoParam + ) + ); + + 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, model); + deepStrictEqual(bodyParam.correspondingMethodParams[0].type, model); + }); + + it("implicit spread", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + op myOp(a: string, b: string): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "myOp"); + strictEqual(method.kind, "basic"); + strictEqual(method.parameters.length, 3); + + const a = method.parameters.find((x) => x.name === "a"); + ok(a); + strictEqual(a.kind, "method"); + strictEqual(a.optional, false); + strictEqual(a.onClient, false); + strictEqual(a.isApiVersionParam, false); + strictEqual(a.type.kind, "string"); + + const b = method.parameters.find((x) => x.name === "b"); + ok(b); + strictEqual(b.kind, "method"); + strictEqual(b.optional, false); + strictEqual(b.onClient, false); + strictEqual(b.isApiVersionParam, false); + strictEqual(b.type.kind, "string"); + + const serviceOperation = method.operation; + const bodyParameter = serviceOperation.bodyParam; + ok(bodyParameter); + + strictEqual(bodyParameter.kind, "body"); + strictEqual(bodyParameter.onClient, false); + strictEqual(bodyParameter.optional, false); + strictEqual(bodyParameter.type, sdkPackage.models[0]); + strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); + strictEqual(bodyParameter.type.access, "internal"); + + strictEqual(bodyParameter.correspondingMethodParams.length, 2); + deepStrictEqual(bodyParameter.correspondingMethodParams[0], a); + deepStrictEqual(bodyParameter.correspondingMethodParams[1], b); + }); + + it("implicit spread with metadata", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + op myOp(@header a: string, b: string): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "myOp"); + strictEqual(method.kind, "basic"); + strictEqual(method.parameters.length, 3); + + const a = method.parameters.find((x) => x.name === "a"); + ok(a); + strictEqual(a.kind, "method"); + strictEqual(a.optional, false); + strictEqual(a.onClient, false); + strictEqual(a.isApiVersionParam, false); + strictEqual(a.type.kind, "string"); + + const b = method.parameters.find((x) => x.name === "b"); + ok(b); + strictEqual(b.kind, "method"); + strictEqual(b.optional, false); + strictEqual(b.onClient, false); + strictEqual(b.isApiVersionParam, false); + strictEqual(b.type.kind, "string"); + + const serviceOperation = method.operation; + const headerParameter = serviceOperation.parameters.find((p) => (p.name = "a")); + ok(headerParameter); + strictEqual(headerParameter.kind, "header"); + strictEqual(headerParameter.onClient, false); + strictEqual(headerParameter.optional, false); + strictEqual(headerParameter.type.kind, "string"); + + strictEqual(headerParameter.correspondingMethodParams.length, 1); + deepStrictEqual(headerParameter.correspondingMethodParams[0], a); + + const bodyParameter = serviceOperation.bodyParam; + ok(bodyParameter); + + strictEqual(bodyParameter.kind, "body"); + strictEqual(bodyParameter.onClient, false); + strictEqual(bodyParameter.optional, false); + strictEqual(bodyParameter.type, sdkPackage.models[0]); + strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); + strictEqual(bodyParameter.type.access, "internal"); + + strictEqual(bodyParameter.correspondingMethodParams.length, 1); + deepStrictEqual(bodyParameter.correspondingMethodParams[0], b); + }); + + it("explicit spread", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + model Test { + a: string; + b: string; + } + op myOp(...Test): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "myOp"); + strictEqual(method.kind, "basic"); + strictEqual(method.parameters.length, 3); + + const a = method.parameters.find((x) => x.name === "a"); + ok(a); + strictEqual(a.kind, "method"); + strictEqual(a.optional, false); + strictEqual(a.onClient, false); + strictEqual(a.isApiVersionParam, false); + strictEqual(a.type.kind, "string"); + + const b = method.parameters.find((x) => x.name === "b"); + ok(b); + strictEqual(b.kind, "method"); + strictEqual(b.optional, false); + strictEqual(b.onClient, false); + strictEqual(b.isApiVersionParam, false); + strictEqual(b.type.kind, "string"); + + const serviceOperation = method.operation; + const bodyParameter = serviceOperation.bodyParam; + ok(bodyParameter); + + strictEqual(bodyParameter.kind, "body"); + strictEqual(bodyParameter.onClient, false); + strictEqual(bodyParameter.optional, false); + strictEqual(bodyParameter.type, sdkPackage.models[0]); + strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); + strictEqual(bodyParameter.type.access, "internal"); + + strictEqual(bodyParameter.correspondingMethodParams.length, 2); + deepStrictEqual(bodyParameter.correspondingMethodParams[0], a); + deepStrictEqual(bodyParameter.correspondingMethodParams[1], b); + }); + + it("explicit spread with metadata", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + model Test { + @header + a: string; + b: string; + } + op myOp(...Test): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "myOp"); + strictEqual(method.kind, "basic"); + strictEqual(method.parameters.length, 3); + + const a = method.parameters.find((x) => x.name === "a"); + ok(a); + strictEqual(a.kind, "method"); + strictEqual(a.optional, false); + strictEqual(a.onClient, false); + strictEqual(a.isApiVersionParam, false); + strictEqual(a.type.kind, "string"); + + const b = method.parameters.find((x) => x.name === "b"); + ok(b); + strictEqual(b.kind, "method"); + strictEqual(b.optional, false); + strictEqual(b.onClient, false); + strictEqual(b.isApiVersionParam, false); + strictEqual(b.type.kind, "string"); + + const serviceOperation = method.operation; + const headerParameter = serviceOperation.parameters.find((p) => (p.name = "a")); + ok(headerParameter); + strictEqual(headerParameter.kind, "header"); + strictEqual(headerParameter.onClient, false); + strictEqual(headerParameter.optional, false); + strictEqual(headerParameter.type.kind, "string"); + + strictEqual(headerParameter.correspondingMethodParams.length, 1); + deepStrictEqual(headerParameter.correspondingMethodParams[0], a); + + const bodyParameter = serviceOperation.bodyParam; + ok(bodyParameter); + + strictEqual(bodyParameter.kind, "body"); + strictEqual(bodyParameter.onClient, false); + strictEqual(bodyParameter.optional, false); + strictEqual(bodyParameter.type, sdkPackage.models[0]); + strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); + strictEqual(bodyParameter.type.access, "internal"); + + strictEqual(bodyParameter.correspondingMethodParams.length, 1); + deepStrictEqual(bodyParameter.correspondingMethodParams[0], b); + }); + + it("explicit multiple spread", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + model Test1 { + a: string; + + } + + model Test2 { + b: string; + } + op myOp(...Test1, ...Test2): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "myOp"); + strictEqual(method.kind, "basic"); + strictEqual(method.parameters.length, 3); + + const a = method.parameters.find((x) => x.name === "a"); + ok(a); + strictEqual(a.kind, "method"); + strictEqual(a.optional, false); + strictEqual(a.onClient, false); + strictEqual(a.isApiVersionParam, false); + strictEqual(a.type.kind, "string"); + + const b = method.parameters.find((x) => x.name === "b"); + ok(b); + strictEqual(b.kind, "method"); + strictEqual(b.optional, false); + strictEqual(b.onClient, false); + strictEqual(b.isApiVersionParam, false); + strictEqual(b.type.kind, "string"); + + const serviceOperation = method.operation; + const bodyParameter = serviceOperation.bodyParam; + ok(bodyParameter); + + strictEqual(bodyParameter.kind, "body"); + strictEqual(bodyParameter.onClient, false); + strictEqual(bodyParameter.optional, false); + strictEqual(bodyParameter.type, sdkPackage.models[0]); + strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); + strictEqual(bodyParameter.type.access, "internal"); + + strictEqual(bodyParameter.correspondingMethodParams.length, 2); + deepStrictEqual(bodyParameter.correspondingMethodParams[0], a); + deepStrictEqual(bodyParameter.correspondingMethodParams[1], b); + }); + + it("explicit multiple spread with metadata", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + model Test1 { + @header + a: string; + } + model Test2 { + b: string; + } + op myOp(...Test1, ...Test2): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "myOp"); + strictEqual(method.kind, "basic"); + strictEqual(method.parameters.length, 3); + + const a = method.parameters.find((x) => x.name === "a"); + ok(a); + strictEqual(a.kind, "method"); + strictEqual(a.optional, false); + strictEqual(a.onClient, false); + strictEqual(a.isApiVersionParam, false); + strictEqual(a.type.kind, "string"); + + const b = method.parameters.find((x) => x.name === "b"); + ok(b); + strictEqual(b.kind, "method"); + strictEqual(b.optional, false); + strictEqual(b.onClient, false); + strictEqual(b.isApiVersionParam, false); + strictEqual(b.type.kind, "string"); + + const serviceOperation = method.operation; + const headerParameter = serviceOperation.parameters.find((p) => (p.name = "a")); + ok(headerParameter); + strictEqual(headerParameter.kind, "header"); + strictEqual(headerParameter.onClient, false); + strictEqual(headerParameter.optional, false); + strictEqual(headerParameter.type.kind, "string"); + + strictEqual(headerParameter.correspondingMethodParams.length, 1); + deepStrictEqual(headerParameter.correspondingMethodParams[0], a); + + const bodyParameter = serviceOperation.bodyParam; + ok(bodyParameter); + + strictEqual(bodyParameter.kind, "body"); + strictEqual(bodyParameter.onClient, false); + strictEqual(bodyParameter.optional, false); + strictEqual(bodyParameter.type, sdkPackage.models[0]); + strictEqual(bodyParameter.type.usage, UsageFlags.Spread | UsageFlags.Json); + strictEqual(bodyParameter.type.access, "internal"); + + strictEqual(bodyParameter.correspondingMethodParams.length, 1); + deepStrictEqual(bodyParameter.correspondingMethodParams[0], b); + }); + + it("spread idempotent", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + alias FooAlias = { + @path id: string; + @doc("name of the Foo") + name: string; + }; + op test(...FooAlias): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + getAllModels(runner.context); + + strictEqual(sdkPackage.models[0].name, "TestRequest"); + strictEqual(sdkPackage.models[0].usage, UsageFlags.Spread | UsageFlags.Json); + }); + + it("model used as simple spread", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + model Test { + prop: string; + } + op test(...Test): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + getAllModels(runner.context); + + strictEqual(sdkPackage.models[0].name, "Test"); + strictEqual(sdkPackage.models[0].usage, UsageFlags.Spread | UsageFlags.Json); + strictEqual(sdkPackage.models[0].access, "internal"); + }); + + it("model used as simple spread and output", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + model Test { + prop: string; + } + op test(...Test): Test; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + getAllModels(runner.context); + + strictEqual(sdkPackage.models[0].name, "Test"); + strictEqual( + sdkPackage.models[0].usage, + UsageFlags.Spread | UsageFlags.Output | UsageFlags.Json + ); + strictEqual(sdkPackage.models[0].access, "public"); + }); + + it("model used as simple spread and other operation's output", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + model Test { + prop: string; + } + op test(...Test): void; + + @route("/another") + op another(): Test; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + getAllModels(runner.context); + + strictEqual(sdkPackage.models[0].name, "Test"); + strictEqual( + sdkPackage.models[0].usage, + UsageFlags.Spread | UsageFlags.Output | UsageFlags.Json + ); + strictEqual(sdkPackage.models[0].access, "public"); + }); + + it("model used as simple spread and other operation's input", async () => { + await runner.compile(`@server("http://localhost:3000", "endpoint") + @service({}) + namespace My.Service; + model Test { + prop: string; + } + op test(...Test): void; + + @route("/another") + op another(@body body: Test): void; + `); + const sdkPackage = runner.context.sdkPackage; + strictEqual(sdkPackage.models.length, 1); + getAllModels(runner.context); + + strictEqual(sdkPackage.models[0].name, "Test"); + strictEqual(sdkPackage.models[0].usage, UsageFlags.Spread | UsageFlags.Input | UsageFlags.Json); + strictEqual(sdkPackage.models[0].access, "public"); + }); +});