Skip to content

Commit

Permalink
Emit decorator list (#3905)
Browse files Browse the repository at this point in the history
Fix #4031

When we specify the allowed-emit-decorators in emitter, TCGC will help
to emit out decoration information for each item (client, property,
operation), e.g final-state-var, our .NET input-Model need to contain
this information.

This PR will 

- emit decorators to all kind of models, operations and operations
- add reference resolve strategy in TypeSpecInputDecoratorInfoConverter
to support parse by reference
  • Loading branch information
chunyu3 authored Aug 15, 2024
1 parent 2eac90c commit 1dc5711
Show file tree
Hide file tree
Showing 45 changed files with 2,253 additions and 1,141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export function createModel(sdkContext: SdkContext<NetEmitterOptions>): CodeMode
Protocol: {},
Parent: parentNames.length > 0 ? parentNames[parentNames.length - 1] : undefined,
Parameters: clientParameters,
Decorators: client.decorators,
};
}

Expand Down
13 changes: 13 additions & 0 deletions packages/http-client-csharp/emitter/src/lib/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export function fromSdkModelType(
flattenedNamePrefixes.length > 0
? flattenedNamePrefixes.concat(property.name)
: undefined,
Decorators: property.decorators,
};

return [modelProperty];
Expand Down Expand Up @@ -213,6 +214,7 @@ export function fromSdkEnumType(
Description: enumType.description,
IsExtensible: enumType.isFixed ? false : true,
Usage: enumType.usage,
Decorators: enumType.decorators,
};
if (addToCollection) enums.set(enumName, newInputEnumType);
inputEnumType = newInputEnumType;
Expand All @@ -228,6 +230,7 @@ function fromSdkDateTimeType(dateTimeType: SdkDateTimeType): InputDateTimeType {
WireType: fromSdkBuiltInType(dateTimeType.wireType),
CrossLanguageDefinitionId: dateTimeType.crossLanguageDefinitionId,
BaseType: dateTimeType.baseType ? fromSdkDateTimeType(dateTimeType.baseType) : undefined,
Decorators: dateTimeType.decorators,
};
}

Expand All @@ -239,6 +242,7 @@ function fromSdkDurationType(durationType: SdkDurationType): InputDurationType {
WireType: fromSdkBuiltInType(durationType.wireType),
CrossLanguageDefinitionId: durationType.crossLanguageDefinitionId,
BaseType: durationType.baseType ? fromSdkDurationType(durationType.baseType) : undefined,
Decorators: durationType.decorators,
};
}

Expand All @@ -248,6 +252,7 @@ function fromTupleType(tupleType: SdkTupleType): InputPrimitiveType {
Kind: "any",
Name: "tuple",
CrossLanguageDefinitionId: "",
Decorators: tupleType.decorators,
};
}

Expand All @@ -258,6 +263,7 @@ function fromSdkBuiltInType(builtInType: SdkBuiltInType): InputPrimitiveType {
Encode: builtInType.encode !== builtInType.kind ? builtInType.encode : undefined, // In TCGC this is required, and when there is no encoding, it just has the same value as kind, we could remove this when TCGC decides to simplify
CrossLanguageDefinitionId: builtInType.crossLanguageDefinitionId,
BaseType: builtInType.baseType ? fromSdkBuiltInType(builtInType.baseType) : undefined,
Decorators: builtInType.decorators,
};
}

Expand All @@ -277,6 +283,7 @@ function fromUnionType(
Kind: "union",
Name: union.name,
VariantTypes: variantTypes,
Decorators: union.decorators,
};
}

Expand All @@ -296,6 +303,7 @@ function fromSdkConstantType(
// we might keep constant as-is, instead of creating an enum for it.
convertConstantToEnum(constantType, enums, literalTypeContext),
Value: constantType.value,
Decorators: constantType.decorators,
};

function convertConstantToEnum(
Expand Down Expand Up @@ -325,6 +333,7 @@ function fromSdkConstantType(
Description: `The ${enumName}`, // TODO -- what should we put here?
IsExtensible: true,
Usage: literalTypeContext.Usage,
Decorators: constantType.decorators,
};
enums.set(enumName, enumType);
return enumType;
Expand All @@ -344,6 +353,7 @@ function fromSdkEnumValueTypeToConstantType(
? fromSdkBuiltInType(enumValueType.valueType as SdkBuiltInType) // TODO: TCGC fix
: fromSdkEnumType(enumValueType.enumType, context, enums),
Value: enumValueType.value,
Decorators: enumValueType.decorators,
};
}

Expand All @@ -352,6 +362,7 @@ function fromSdkEnumValueType(enumValueType: SdkEnumValueType): InputEnumTypeVal
Name: enumValueType.name,
Value: enumValueType.value,
Description: enumValueType.description,
Decorators: enumValueType.decorators,
};
}

Expand All @@ -365,6 +376,7 @@ function fromSdkDictionaryType(
Kind: "dict",
KeyType: fromSdkType(dictionaryType.keyType, context, models, enums),
ValueType: fromSdkType(dictionaryType.valueType, context, models, enums),
Decorators: dictionaryType.decorators,
};
}

Expand All @@ -379,6 +391,7 @@ function fromSdkArrayType(
Name: arrayType.name,
ValueType: fromSdkType(arrayType.valueType, context, models, enums),
CrossLanguageDefinitionId: arrayType.crossLanguageDefinitionId,
Decorators: arrayType.decorators,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export function fromSdkServiceMethod(
GenerateProtocolMethod: shouldGenerateProtocol(sdkContext, method.operation.__raw.operation),
GenerateConvenienceMethod: generateConvenience,
CrossLanguageDefinitionId: method.crossLanguageDefintionId,
Decorators: method.decorators,
};
}

Expand Down Expand Up @@ -203,6 +204,7 @@ function fromSdkHttpOperationParameter(
IsRequired: !p.optional,
Kind: getParameterKind(p, parameterType, rootApiVersions.length > 0),
DefaultValue: getParameterDefaultValue(p.clientDefaultValue, parameterType),
Decorators: p.decorators,
} as InputParameter;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/http-client-csharp/emitter/src/type/input-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { DecoratorInfo } from "@azure-tools/typespec-client-generator-core";
import { InputOperation } from "./input-operation.js";
import { InputParameter } from "./input-parameter.js";
import { Protocols } from "./protocols.js";
Expand All @@ -12,4 +13,5 @@ export interface InputClient {
Protocol?: Protocols;
Parent?: string;
Parameters?: InputParameter[];
Decorators?: DecoratorInfo[];
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { DecoratorInfo } from "@azure-tools/typespec-client-generator-core";

export interface InputEnumTypeValue {
Name: string;
Value: any;
Description?: string;
Decorators?: DecoratorInfo[];
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { DecoratorInfo } from "@azure-tools/typespec-client-generator-core";
import { InputType } from "./input-type.js";

export interface InputModelProperty {
Expand All @@ -12,4 +13,5 @@ export interface InputModelProperty {
IsReadOnly: boolean;
IsDiscriminator?: boolean;
FlattenedNames?: string[];
Decorators?: DecoratorInfo[];
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { DecoratorInfo } from "@azure-tools/typespec-client-generator-core";
import { BodyMediaType } from "./body-media-type.js";
import { InputParameter } from "./input-parameter.js";
import { OperationLongRunning } from "./operation-long-running.js";
Expand Down Expand Up @@ -35,4 +36,5 @@ export interface InputOperation {
GenerateProtocolMethod: boolean;
GenerateConvenienceMethod: boolean;
CrossLanguageDefinitionId: string;
Decorators?: DecoratorInfo[];
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { DecoratorInfo } from "@azure-tools/typespec-client-generator-core";
import { InputConstant } from "./input-constant.js";
import { InputOperationParameterKind } from "./input-operation-parameter-kind.js";
import { InputType } from "./input-type.js";
Expand All @@ -27,4 +28,5 @@ export interface InputParameter {
Explode: boolean;
ArraySerializationDelimiter?: string;
HeaderCollectionPrefix?: string;
Decorators?: DecoratorInfo[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { TestHost } from "@typespec/compiler/testing";
import { deepStrictEqual, strictEqual } from "assert";
import { beforeEach, describe, it } from "vitest";
import { createModel } from "../../src/lib/client-model-builder.js";
import {
createEmitterContext,
createEmitterTestHost,
createNetSdkContext,
typeSpecCompile,
} from "./utils/test-util.js";

describe("Test emitting decorator list", () => {
let runner: TestHost;

beforeEach(async () => {
runner = await createEmitterTestHost();
});

it("emit decorator list on a client", async () => {
const program = await typeSpecCompile(
`
@clientName("CsharpBookClient")
interface BookClient {
op test(): void;
}
`,
runner,
{ IsTCGCNeeded: true, IsXmlNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = await createNetSdkContext(context, {
additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@clientName"],
});
const root = createModel(sdkContext);
const clients = root.Clients;
strictEqual(clients.length, 2);
deepStrictEqual(clients[1].Decorators, [
{
name: "Azure.ClientGenerator.Core.@clientName",
arguments: {
rename: "CsharpBookClient",
},
},
]);
});

it("emit decorator list on a operation", async () => {
const program = await typeSpecCompile(
`
model Book {
content: string;
}
@clientName("ClientTestOperation")
op test(): Book;
`,
runner,
{ IsTCGCNeeded: true, IsXmlNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = await createNetSdkContext(context, {
additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@clientName"],
});
const root = createModel(sdkContext);
const operations = root.Clients[0].Operations;
strictEqual(operations.length, 1);
deepStrictEqual(operations[0].Decorators, [
{
name: "Azure.ClientGenerator.Core.@clientName",
arguments: {
rename: "ClientTestOperation",
},
},
]);
});

it("emit decorator list on a model", async () => {
const program = await typeSpecCompile(
`
@clientName("ClientBook")
model Book {
content: string;
}
op test(): Book;
`,
runner,
{ IsTCGCNeeded: true, IsXmlNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = await createNetSdkContext(context, {
additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@clientName"],
});
const root = createModel(sdkContext);
const models = root.Models;
strictEqual(models.length, 1);
deepStrictEqual(models[0].Decorators, [
{
name: "Azure.ClientGenerator.Core.@clientName",
arguments: {
rename: "ClientBook",
},
},
]);
});

it("emit decorator list on a model property", async () => {
const program = await typeSpecCompile(
`
model Book {
@clientName("ClientContent")
content: string;
}
op test(): Book;
`,
runner,
{ IsTCGCNeeded: true, IsXmlNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = await createNetSdkContext(context, {
additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@clientName"],
});
const root = createModel(sdkContext);
const models = root.Models;
strictEqual(models.length, 1);
deepStrictEqual(models[0].Properties[0].Decorators, [
{
name: "Azure.ClientGenerator.Core.@clientName",
arguments: {
rename: "ClientContent",
},
},
]);
});

it("emit decorator list on a parameter", async () => {
const program = await typeSpecCompile(
`
@clientName("ClientTestOperation")
op test(@clientName("ClientId") @header id: string): void;
`,
runner,
{ IsTCGCNeeded: true, IsXmlNeeded: true }
);
const context = createEmitterContext(program);
const sdkContext = await createNetSdkContext(context, {
additionalDecorators: ["Azure\\.ClientGenerator\\.Core\\.@clientName"],
});
const root = createModel(sdkContext);
const operations = root.Clients[0].Operations;
strictEqual(operations.length, 1);
const idParameters = operations[0].Parameters.filter((p) => p.Name === "ClientId");
strictEqual(idParameters.length, 1);
deepStrictEqual(idParameters[0].Decorators, [
{
name: "Azure.ClientGenerator.Core.@clientName",
arguments: {
rename: "ClientId",
},
},
]);
});
});
11 changes: 8 additions & 3 deletions packages/http-client-csharp/emitter/test/Unit/utils/test-util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing";
import { createSdkContext, SdkContext } from "@azure-tools/typespec-client-generator-core";
import {
createSdkContext,
CreateSdkContextOptions,
SdkContext,
} from "@azure-tools/typespec-client-generator-core";
import { SdkTestLibrary } from "@azure-tools/typespec-client-generator-core/testing";
import {
CompilerOptions,
Expand Down Expand Up @@ -131,8 +135,9 @@ export function navigateModels(

/* We always need to pass in the emitter name now that it is required so making a helper to do this. */
export async function createNetSdkContext(
program: EmitContext<NetEmitterOptions>
program: EmitContext<NetEmitterOptions>,
sdkContextOptions: CreateSdkContextOptions = {}
): Promise<SdkContext<NetEmitterOptions>> {
Logger.initialize(program.program, LoggerLevel.INFO);
return await createSdkContext(program, "@typespec/http-client-csharp");
return await createSdkContext(program, "@typespec/http-client-csharp", sdkContextOptions);
}
Loading

0 comments on commit 1dc5711

Please sign in to comment.