diff --git a/packages/typespec-ts/package.json b/packages/typespec-ts/package.json index ebfbcffb73..750a53a9ab 100644 --- a/packages/typespec-ts/package.json +++ b/packages/typespec-ts/package.json @@ -69,7 +69,7 @@ "@typespec/spector": "0.1.0-alpha.24-dev.2", "@typespec/spec-api": "0.1.0-dev.0", "@typespec/tspd": "0.74.0", - "@azure-tools/azure-http-specs": "0.1.0-alpha.38-dev.2", + "@azure-tools/azure-http-specs": "0.1.0-alpha.38-dev.6", "@azure-tools/typespec-autorest": "^0.65.0", "@azure-tools/typespec-azure-core": "^0.65.0", "@azure-tools/typespec-azure-resource-manager": "^0.65.0", diff --git a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts index 1f05786480..9d25a818d5 100644 --- a/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts +++ b/packages/typespec-ts/src/modular/serialization/buildSerializerFunction.ts @@ -501,6 +501,16 @@ function buildModelTypeSerializer( output.push(` return ${serializeContent} `); + } else if (options.flatten) { + // Private flatten serializer: all child properties are read-only, so nothing to serialize. + // Rename the parameter to _item to avoid TypeScript unused-variable errors. + const firstParam = serializerFunction.parameters?.[0]; + if (firstParam) { + firstParam.name = "_item"; + } + output.push(` + return {}; + `); } else { output.push(` return item; diff --git a/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts b/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts index d99d60a3c2..2639731cc1 100644 --- a/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts +++ b/packages/typespec-ts/test/azureIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts @@ -44,6 +44,16 @@ export declare type FlattenPropertyClient = Client & { export declare interface FlattenPropertyClientOptions extends ClientOptions { } +export declare interface FlattenUnknownModel { + name: string; + properties?: unknown; +} + +export declare interface FlattenUnknownModelOutput { + name: string; + properties?: any; +} + export declare interface NestedFlattenModel { name: string; properties: ChildFlattenModel; @@ -69,6 +79,36 @@ export declare interface PutFlattenModelBodyParam { export declare type PutFlattenModelParameters = PutFlattenModelBodyParam & RequestParameters; +export declare interface PutFlattenReadOnlyModel { + put(options: PutFlattenReadOnlyModelParameters): StreamableMethod; +} + +export declare interface PutFlattenReadOnlyModel200Response extends HttpResponse { + status: "200"; + body: SolutionOutput; +} + +export declare interface PutFlattenReadOnlyModelBodyParam { + body: Solution; +} + +export declare type PutFlattenReadOnlyModelParameters = PutFlattenReadOnlyModelBodyParam & RequestParameters; + +export declare interface PutFlattenUnknownModel { + put(options: PutFlattenUnknownModelParameters): StreamableMethod; +} + +export declare interface PutFlattenUnknownModel200Response extends HttpResponse { + status: "200"; + body: FlattenUnknownModelOutput; +} + +export declare interface PutFlattenUnknownModelBodyParam { + body: FlattenUnknownModel; +} + +export declare type PutFlattenUnknownModelParameters = PutFlattenUnknownModelBodyParam & RequestParameters; + export declare interface PutNestedFlattenModel { put(options: PutNestedFlattenModelParameters): StreamableMethod; } @@ -87,6 +127,27 @@ export declare type PutNestedFlattenModelParameters = PutNestedFlattenModelBodyP export declare interface Routes { (path: "/azure/client-generator-core/flatten-property/flattenModel"): PutFlattenModel; (path: "/azure/client-generator-core/flatten-property/nestedFlattenModel"): PutNestedFlattenModel; + (path: "/azure/client-generator-core/flatten-property/flattenUnknownModel"): PutFlattenUnknownModel; + (path: "/azure/client-generator-core/flatten-property/flattenReadOnlyModel"): PutFlattenReadOnlyModel; +} + +export declare interface Solution { + name: string; + properties?: SolutionProperties; +} + +export declare interface SolutionOutput { + name: string; + properties?: SolutionPropertiesOutput; +} + +export declare interface SolutionProperties { +} + +export declare interface SolutionPropertiesOutput { + readonly solutionId?: string; + readonly title?: string; + readonly content?: string; } export { } diff --git a/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts b/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts index 2c9fda5a33..2bd0677430 100644 --- a/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts +++ b/packages/typespec-ts/test/azureIntegration/modelFlatten.spec.ts @@ -50,4 +50,37 @@ describe("Flatten Property Rest Client", () => { assert.strictEqual(result.body.properties.properties.description, "foo"); assert.strictEqual(result.body.properties.properties.age, 1); }); + + it("should update and receive model with unknown flatten property", async () => { + const result = await client + .path("/azure/client-generator-core/flatten-property/flattenUnknownModel") + .put({ + body: { + name: "foo" + } + }); + assert.strictEqual(result.status, "200"); + assert.strictEqual(result.body.name, "test"); + assert.deepEqual(result.body.properties, { + key1: "value1", + key2: "value2" + }); + }); + + it("should update and receive model with all readonly flatten properties", async () => { + const result = await client + .path( + "/azure/client-generator-core/flatten-property/flattenReadOnlyModel" + ) + .put({ + body: { + name: "foo" + } + }); + assert.strictEqual(result.status, "200"); + assert.strictEqual(result.body.name, "foo"); + assert.strictEqual(result.body.properties?.solutionId, "solution1"); + assert.strictEqual(result.body.properties?.title, "Solution Title"); + assert.strictEqual(result.body.properties?.content, "Solution Content"); + }); }); diff --git a/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts b/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts index 64fd799fc6..a6f598fefc 100644 --- a/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts +++ b/packages/typespec-ts/test/azureModularIntegration/generated/azure/client-generator-core/flatten-property/src/index.d.ts @@ -23,6 +23,8 @@ export declare class FlattenPropertyClient { private _client; readonly pipeline: Pipeline; constructor(options?: FlattenPropertyClientOptionalParams); + putFlattenReadOnlyModel(body: Solution, options?: PutFlattenReadOnlyModelOptionalParams): Promise; + putFlattenUnknownModel(input: FlattenUnknownModel, options?: PutFlattenUnknownModelOptionalParams): Promise; putNestedFlattenModel(input: NestedFlattenModel, options?: PutNestedFlattenModelOptionalParams): Promise; putFlattenModel(input: FlattenModel, options?: PutFlattenModelOptionalParams): Promise; } @@ -30,6 +32,11 @@ export declare class FlattenPropertyClient { export declare interface FlattenPropertyClientOptionalParams extends ClientOptions { } +export declare interface FlattenUnknownModel { + name: string; + properties?: any; +} + export declare interface NestedFlattenModel { name: string; summary: string; @@ -39,7 +46,26 @@ export declare interface NestedFlattenModel { export declare interface PutFlattenModelOptionalParams extends OperationOptions { } +export declare interface PutFlattenReadOnlyModelOptionalParams extends OperationOptions { +} + +export declare interface PutFlattenUnknownModelOptionalParams extends OperationOptions { +} + export declare interface PutNestedFlattenModelOptionalParams extends OperationOptions { } +export declare interface Solution { + name: string; + readonly solutionId?: string; + readonly title?: string; + readonly content?: string; +} + +export declare interface SolutionProperties { + readonly solutionId?: string; + readonly title?: string; + readonly content?: string; +} + export { } diff --git a/packages/typespec-ts/test/azureModularIntegration/modelFlatten.spec.ts b/packages/typespec-ts/test/azureModularIntegration/modelFlatten.spec.ts index 81c01ab753..69b93bfb53 100644 --- a/packages/typespec-ts/test/azureModularIntegration/modelFlatten.spec.ts +++ b/packages/typespec-ts/test/azureModularIntegration/modelFlatten.spec.ts @@ -35,4 +35,23 @@ describe("Property Flatten Client", () => { assert.strictEqual(result.properties.description, "foo"); assert.strictEqual(result.properties.age, 1); }); + + it("Update and receive model with unknown properties flattening", async () => { + const result = await client.putFlattenUnknownModel({ + name: "foo" + }); + assert.strictEqual(result.name, "test"); + assert.strictEqual(result.properties?.key1, "value1"); + assert.strictEqual(result.properties?.key2, "value2"); + }); + + it("Update and receive model with read-only properties flattening", async () => { + const result = await client.putFlattenReadOnlyModel({ + name: "foo" + }); + assert.strictEqual(result.name, "foo"); + assert.strictEqual(result.solutionId, "solution1"); + assert.strictEqual(result.title, "Solution Title"); + assert.strictEqual(result.content, "Solution Content"); + }); }); diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md index 1c0f541a59..a1bf86bf31 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/readonlyFlattenModel.md @@ -73,11 +73,94 @@ export function solutionPropertiesSerializer(item: SolutionProperties): any { return item; } +export function _solutionPropertiesSerializer(_item: Solution): any { + return {}; +} + +export function _solutionPropertiesOptionalSerializer(_item: Solution): any { + return {}; +} +``` + +# Should handle flatten model with not all readonly properties correctly + +## TypeSpec + +This is tsp definition. + +```tsp + +model SolutionProperties { + solutionId?: string; + title?: string; + @visibility(Lifecycle.Read) + content?: string; +} +model Solution{ + @Azure.ClientGenerator.Core.Legacy.flattenProperty + properties: SolutionProperties; + @Azure.ClientGenerator.Core.Legacy.flattenProperty + propertiesOptional?: SolutionProperties; +} +op test(@body body:Solution):void; + +``` + +Enable the raw content with TCGC dependency. + +```yaml +needTCGC: true +``` + +## Models + +```ts models +import { areAllPropsUndefined } from "../static-helpers/serialization/check-prop-undefined.js"; + +/** + * This file contains only generated model types and their (de)serializers. + * Disable the following rules for internal models with '_' prefix and deserializers which require 'any' for raw JSON input. + */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/** model interface Solution */ +export interface Solution { + solutionId?: string; + title?: string; + readonly content?: string; + solutionIdPropertiesOptionalSolutionId?: string; + titlePropertiesOptionalTitle?: string; + readonly contentPropertiesOptionalContent?: string; +} + +export function solutionSerializer(item: Solution): any { + return { + properties: _solutionPropertiesSerializer(item), + propertiesOptional: areAllPropsUndefined(item, ["solutionId", "title"]) + ? undefined + : _solutionPropertiesOptionalSerializer(item), + }; +} + +/** model interface SolutionProperties */ +export interface SolutionProperties { + solutionId?: string; + title?: string; + readonly content?: string; +} + +export function solutionPropertiesSerializer(item: SolutionProperties): any { + return { solutionId: item["solutionId"], title: item["title"] }; +} + export function _solutionPropertiesSerializer(item: Solution): any { - return item; + return { solutionId: item["solutionId"], title: item["title"] }; } export function _solutionPropertiesOptionalSerializer(item: Solution): any { - return item; + return { + solutionId: item["solutionIdPropertiesOptionalSolutionId"], + title: item["titlePropertiesOptionalTitle"], + }; } ``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ec4c3e798..72f9741b5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -364,8 +364,8 @@ importers: specifier: ^2.3.1 version: 2.5.1 '@azure-tools/azure-http-specs': - specifier: 0.1.0-alpha.38-dev.2 - version: 0.1.0-alpha.38-dev.2(d5a39e74174d9ff5d871e042fc1520d9) + specifier: 0.1.0-alpha.38-dev.6 + version: 0.1.0-alpha.38-dev.6(d5a39e74174d9ff5d871e042fc1520d9) '@azure-tools/typespec-autorest': specifier: ^0.65.0 version: 0.65.0(f818ba7313efc9695bc0d8b1fa87588e) @@ -534,13 +534,13 @@ packages: resolution: {integrity: sha512-X1C7XdyCuo50ch9FzKtTvmK18FgDxxf1Bbt3cSoknQqeDaRegHSSCO+zByq2YA4NvUzKXeZ1engh29IDxZXgpQ==} engines: {node: '>=10.12.0'} - '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.2': - resolution: {integrity: sha512-rrxSHv70c5d9ZZQRN1xZ849/vzAFM7uHEkpjSNtKBBkx/6/2J2arSiltk4OP6xE/lqHPVPeANzbYQsL4b4wNZw==} + '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.6': + resolution: {integrity: sha512-MXjnMSArEM1+ADCmSrYDsOlTzL9bQWaVMtrTtOK8+2eAL87dZdfxiFuiPr9jWHYC8m8nIwLZ+h4dE7TSQR0KwQ==} engines: {node: '>=20.0.0'} peerDependencies: '@azure-tools/typespec-azure-core': ^0.65.0 || >=0.66.0-dev <0.66.0 '@typespec/compiler': ^1.9.0 - '@typespec/http': ^1.9.0 + '@typespec/http': ^1.9.1 '@typespec/rest': ^0.79.0 || >=0.80.0-dev <0.80.0 '@typespec/versioning': ^0.79.0 || >=0.80.0-dev <0.80.0 '@typespec/xml': ^0.79.0 || >=0.80.0-dev <0.80.0 @@ -5576,7 +5576,7 @@ snapshots: '@azure-tools/tasks': 3.0.255 proper-lockfile: 2.0.1 - '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.2(d5a39e74174d9ff5d871e042fc1520d9)': + '@azure-tools/azure-http-specs@0.1.0-alpha.38-dev.6(d5a39e74174d9ff5d871e042fc1520d9)': dependencies: '@azure-tools/typespec-azure-core': 0.65.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/http@1.9.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/streams@0.77.0(@typespec/compiler@1.9.0(@types/node@25.0.8))))(@typespec/rest@0.79.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/http@1.9.0(@typespec/compiler@1.9.0(@types/node@25.0.8))(@typespec/streams@0.77.0(@typespec/compiler@1.9.0(@types/node@25.0.8))))) '@typespec/compiler': 1.9.0(@types/node@25.0.8)