Skip to content

Commit 37b0639

Browse files
Refactor emitter structure and fix a few minor issues (Azure#49905)
1 parent ae76b26 commit 37b0639

File tree

8 files changed

+323
-211
lines changed

8 files changed

+323
-211
lines changed

eng/packages/http-client-csharp-mgmt/emitter/src/emitter.ts

Lines changed: 4 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,105 +2,27 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
import { EmitContext } from "@typespec/compiler";
5-
import { DecoratorInfo } from "@azure-tools/typespec-client-generator-core";
65

7-
import {
8-
CodeModel,
9-
InputClient,
10-
InputModelType,
11-
} from "@typespec/http-client-csharp";
6+
import { CodeModel } from "@typespec/http-client-csharp";
127

138
import {
149
$onEmit as $onAzureEmit,
1510
AzureEmitterOptions
1611
} from "@azure-typespec/http-client-csharp";
1712
import { azureSDKContextOptions } from "./sdk-context-options.js";
18-
import { calculateResourceTypeFromPath } from "./resource-type.js";
19-
20-
const armResourceOperations = "Azure.ResourceManager.@armResourceOperations";
21-
const armResourceRead = "Azure.ResourceManager.@armResourceRead";
22-
const armResourceCreateOrUpdate =
23-
"Azure.ResourceManager.@armResourceCreateOrUpdate";
24-
const singleton = "Azure.ResourceManager.@singleton";
25-
const resourceMetadata = "Azure.ClientGenerator.Core.@resourceSchema";
13+
import { updateClients } from "./resource-detection.js";
2614

2715
export async function $onEmit(context: EmitContext<AzureEmitterOptions>) {
2816
context.options["generator-name"] ??= "ManagementClientGenerator";
2917
context.options["update-code-model"] = updateCodeModel;
3018
context.options["emitter-extension-path"] ??= import.meta.url;
3119
context.options["sdk-context-options"] ??= azureSDKContextOptions;
32-
context.options["model-namespace"] ??= true;
20+
context.options["model-namespace"] ??= true;
3321
await $onAzureEmit(context);
3422
}
3523

3624
function updateCodeModel(codeModel: CodeModel): CodeModel {
37-
for (const client of codeModel.clients) {
38-
updateClient(client);
39-
}
40-
41-
function updateClient(client: InputClient) {
42-
// TODO: we can implement this decorator in TCGC until we meet the corner case
43-
// if the client has resourceMetadata decorator, it is a resource client and we don't need to add it again
44-
if (client.decorators?.some((d) => d.name == resourceMetadata)) {
45-
return;
46-
}
47-
48-
// TODO: Once we have the ability to get resource hierarchy from TCGC directly, we can remove this implementation
49-
// A resource client should have decorator armResourceOperations and contains either a get operation(containing armResourceRead deocrator) or a put operation(containing armResourceCreateOrUpdate decorator)
50-
if (
51-
client.decorators?.some((d) => d.name == armResourceOperations) &&
52-
client.methods.some(
53-
(m) =>
54-
m.operation.decorators?.some(
55-
(d) => d.name == armResourceRead || armResourceCreateOrUpdate
56-
)
57-
)
58-
) {
59-
let resourceModel: InputModelType | undefined = undefined;
60-
let isSingleton: boolean = false;
61-
let resourceType: string | undefined = undefined;
62-
// We will try to get resource metadata from put operation firstly, if not found, we will try to get it from get operation
63-
const putOperation = client.methods.find(
64-
(m) => m.operation.decorators?.some((d) => d.name == armResourceCreateOrUpdate)
65-
)?.operation;
66-
if (putOperation) {
67-
const path = putOperation.path;
68-
resourceType = calculateResourceTypeFromPath(path);
69-
resourceModel = putOperation.responses.filter((r) => r.bodyType)[0]
70-
.bodyType as InputModelType;
71-
isSingleton =
72-
resourceModel.decorators?.some((d) => d.name == singleton) ?? false;
73-
} else {
74-
const getOperation = client.methods.find(
75-
(m) => m.operation.decorators?.some((d) => d.name == armResourceRead)
76-
)?.operation;
77-
if (getOperation) {
78-
const path = getOperation.path;
79-
resourceType = calculateResourceTypeFromPath(path);
80-
resourceModel = getOperation.responses.filter((r) => r.bodyType)[0]
81-
.bodyType as InputModelType;
82-
isSingleton =
83-
resourceModel.decorators?.some((d) => d.name == singleton) ?? false;
84-
}
85-
}
86-
87-
const resourceMetadataDecorator: DecoratorInfo = {
88-
name: resourceMetadata,
89-
arguments: {}
90-
};
91-
resourceMetadataDecorator.arguments["resourceModel"] =
92-
resourceModel?.crossLanguageDefinitionId;
93-
resourceMetadataDecorator.arguments["isSingleton"] =
94-
isSingleton.toString();
95-
resourceMetadataDecorator.arguments["resourceType"] = resourceType;
96-
client.decorators.push(resourceMetadataDecorator);
97-
}
25+
updateClients(codeModel);
9826

99-
if (client.children) {
100-
for (const child of client.children) {
101-
updateClient(child as InputClient);
102-
}
103-
}
104-
}
10527
return codeModel;
10628
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
import {
5+
CodeModel,
6+
InputClient,
7+
InputModelType
8+
} from "@typespec/http-client-csharp";
9+
import {
10+
calculateResourceTypeFromPath,
11+
ResourceMetadata
12+
} from "./resource-metadata.js";
13+
import { DecoratorInfo } from "@azure-tools/typespec-client-generator-core";
14+
import {
15+
armResourceCreateOrUpdate,
16+
armResourceOperations,
17+
armResourceRead,
18+
resourceMetadata,
19+
singleton
20+
} from "./sdk-context-options.js";
21+
22+
export function updateClients(codeModel: CodeModel) {
23+
// first we flatten all possible clients in the code model
24+
const clients = getAllClients(codeModel);
25+
26+
// to fully calculation the resource metadata, we have to go with 2 passes
27+
// in which the first pass we gather everything we could for each client
28+
// the second pass we figure out there cross references between the clients (such as parent resource)
29+
// then pass to update all the clients with their own information
30+
const metadata: Map<InputClient, ResourceMetadata> = new Map();
31+
for (const client of clients) {
32+
gatherResourceMetadata(client, metadata);
33+
}
34+
35+
// populate the parent resource information
36+
37+
// the last step, add the decorator to the client
38+
for (const client of clients) {
39+
const resourceMetadata = metadata.get(client);
40+
if (resourceMetadata) {
41+
addResourceMetadata(client, resourceMetadata);
42+
}
43+
}
44+
}
45+
46+
function getAllClients(codeModel: CodeModel): InputClient[] {
47+
const clients: InputClient[] = [];
48+
for (const client of codeModel.clients) {
49+
traverseClient(client);
50+
}
51+
52+
return clients;
53+
54+
function traverseClient(client: InputClient) {
55+
clients.push(client);
56+
if (client.children) {
57+
for (const child of client.children) {
58+
traverseClient(child);
59+
}
60+
}
61+
}
62+
}
63+
64+
function gatherResourceMetadata(
65+
client: InputClient,
66+
metadataMap: Map<InputClient, ResourceMetadata>
67+
) {
68+
// TODO: we can implement this decorator in TCGC until we meet the corner case
69+
// if the client has resourceMetadata decorator, it is a resource client and we don't need to add it again
70+
if (client.decorators?.some((d) => d.name == resourceMetadata)) {
71+
return;
72+
}
73+
74+
// TODO: Once we have the ability to get resource hierarchy from TCGC directly, we can remove this implementation
75+
// A resource client should have decorator armResourceOperations and contains either a get operation(containing armResourceRead deocrator) or a put operation(containing armResourceCreateOrUpdate decorator)
76+
if (
77+
client.decorators?.some((d) => d.name == armResourceOperations) &&
78+
client.methods.some(
79+
(m) =>
80+
m.operation.decorators?.some(
81+
(d) => d.name == armResourceRead || armResourceCreateOrUpdate
82+
)
83+
)
84+
) {
85+
let resourceModel: InputModelType | undefined = undefined;
86+
let isSingleton: boolean = false;
87+
let resourceType: string | undefined = undefined;
88+
// We will try to get resource metadata from put operation firstly, if not found, we will try to get it from get operation
89+
const putOperation = client.methods.find(
90+
(m) =>
91+
m.operation.decorators?.some((d) => d.name == armResourceCreateOrUpdate)
92+
)?.operation;
93+
if (putOperation) {
94+
const path = putOperation.path;
95+
resourceType = calculateResourceTypeFromPath(path);
96+
resourceModel = putOperation.responses.filter((r) => r.bodyType)[0]
97+
.bodyType as InputModelType;
98+
isSingleton =
99+
resourceModel.decorators?.some((d) => d.name == singleton) ?? false;
100+
} else {
101+
const getOperation = client.methods.find(
102+
(m) => m.operation.decorators?.some((d) => d.name == armResourceRead)
103+
)?.operation;
104+
if (getOperation) {
105+
const path = getOperation.path;
106+
resourceType = calculateResourceTypeFromPath(path);
107+
resourceModel = getOperation.responses.filter((r) => r.bodyType)[0]
108+
.bodyType as InputModelType;
109+
isSingleton =
110+
resourceModel.decorators?.some((d) => d.name == singleton) ?? false;
111+
}
112+
}
113+
114+
if (resourceModel && resourceType) {
115+
const metadata = {
116+
resourceModel: resourceModel,
117+
resourceClient: client,
118+
resourceType: resourceType,
119+
isSingleton: isSingleton
120+
};
121+
metadataMap.set(client, metadata);
122+
}
123+
}
124+
}
125+
126+
function addResourceMetadata(client: InputClient, metadata: ResourceMetadata) {
127+
const resourceMetadataDecorator: DecoratorInfo = {
128+
name: resourceMetadata,
129+
arguments: {
130+
resourceModel: metadata.resourceModel.crossLanguageDefinitionId,
131+
isSingleton: metadata.isSingleton,
132+
resourceType: metadata.resourceType
133+
}
134+
};
135+
136+
if (!client.decorators) {
137+
client.decorators = [];
138+
}
139+
140+
client.decorators.push(resourceMetadataDecorator);
141+
}

eng/packages/http-client-csharp-mgmt/emitter/src/resource-type.ts renamed to eng/packages/http-client-csharp-mgmt/emitter/src/resource-metadata.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4+
import { InputClient, InputModelType } from "@typespec/http-client-csharp";
5+
46
const ResourceGroupScopePrefix =
57
"/subscriptions/{subscriptionId}/resourceGroups";
68
const SubscriptionScopePrefix = "/subscriptions";
@@ -29,3 +31,10 @@ export function calculateResourceTypeFromPath(path: string): string {
2931
else return result;
3032
}, "");
3133
}
34+
35+
export interface ResourceMetadata {
36+
resourceType: string;
37+
resourceModel: InputModelType;
38+
resourceClient: InputClient;
39+
isSingleton: boolean;
40+
}

eng/packages/http-client-csharp-mgmt/emitter/src/sdk-context-options.ts

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,57 @@
33

44
import { CreateSdkContextOptions } from "@azure-tools/typespec-client-generator-core";
55

6+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-client-generator-core/README.md#usesystemtextjsonconverter
7+
const useSystemTextJsonConverterRegex =
8+
"Azure\\.ClientGenerator\\.Core\\.@useSystemTextJsonConverter";
9+
10+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armprovidernamespace
11+
const armProviderNamespaceRegex =
12+
"Azure\\.ResourceManager\\.@armProviderNamespace";
13+
14+
// https://github.com/microsoft/typespec/blob/main/packages/rest/README.md#parentresource
15+
export const parentResource = "TypeSpec.Rest.@parentResource";
16+
const parentResourceRegex = "TypeSpec\\.Rest\\.@parentResource";
17+
18+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armresourceoperations
19+
export const armResourceOperations =
20+
"Azure.ResourceManager.@armResourceOperations";
21+
const armResourceOperationsRegex =
22+
"Azure\\.ResourceManager\\.@armResourceOperations";
23+
24+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armResourceRead
25+
export const armResourceRead = "Azure.ResourceManager.@armResourceRead";
26+
const armResourceReadRegex = "Azure\\.ResourceManager\\.@armResourceRead";
27+
28+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armresourcecreateorupdate
29+
export const armResourceCreateOrUpdate =
30+
"Azure.ResourceManager.@armResourceCreateOrUpdate";
31+
const armResourceCreateOrUpdateRegex =
32+
"Azure\\.ResourceManager\\.@armResourceCreateOrUpdate";
33+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#singleton
34+
export const singleton = "Azure.ResourceManager.@singleton";
35+
const singletonRegex = "Azure\\.ResourceManager\\.@singleton";
36+
37+
const armResourceInternalRegex = "Azure\\.ResourceManager\\.Private\\.@armResourceInternal";
38+
39+
// TODO: add this decorator to TCGC
40+
export const resourceMetadata = "Azure.ClientGenerator.Core.@resourceSchema";
41+
const resourceMetadataRegex =
42+
"Azure\\.ClientGenerator\\.Core\\.@resourceSchema";
43+
644
export const azureSDKContextOptions: CreateSdkContextOptions = {
745
versioning: {
8-
previewStringRegex: /-preview$/,
46+
previewStringRegex: /-preview$/
947
},
1048
additionalDecorators: [
11-
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-client-generator-core/README.md#usesystemtextjsonconverter
12-
"Azure\\.ClientGenerator\\.Core\\.@useSystemTextJsonConverter",
13-
// TODO: add this decorator to TCGC
14-
"Azure\\.ClientGenerator\\.Core\\.@resourceSchema",
15-
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armprovidernamespace
16-
"Azure\\.ResourceManager\\.@armProviderNamespace",
17-
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armresourceoperations
18-
"Azure\\.ResourceManager\\.@armResourceOperations",
19-
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armResourceRead
20-
"Azure\\.ResourceManager\\.@armResourceRead",
21-
// https://github.com/microsoft/typespec/blob/main/packages/rest/README.md#parentresource
22-
"TypeSpec\\.Rest\\.parentResource",
23-
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#singleton
24-
"Azure\\.ResourceManager\\.@singleton",
25-
"Azure\\.ResourceManager\\.Private\\.@armResourceInternal"
49+
useSystemTextJsonConverterRegex,
50+
resourceMetadataRegex,
51+
armProviderNamespaceRegex,
52+
armResourceOperationsRegex,
53+
armResourceCreateOrUpdateRegex,
54+
armResourceReadRegex,
55+
parentResourceRegex,
56+
singletonRegex,
57+
armResourceInternalRegex,
2658
]
2759
};

eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Mgmt/src/ManagementOutputLibrary.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ private static void BuildResourceCore(List<ResourceClientProvider> resources, Li
3838
var resource = new ResourceClientProvider(client);
3939
ManagementClientGenerator.Instance.AddTypeToKeep(resource.Name);
4040
resources.Add(resource);
41-
var isSingleton = resourceMetadata.Arguments?.TryGetValue("isSingleton", out var result) == true ? result.ToObjectFromJson<string>() == "true" : false;
42-
if (!isSingleton)
41+
if (!resource.IsSingleton)
4342
{
4443
var collection = new ResourceCollectionClientProvider(client, resource);
4544
ManagementClientGenerator.Instance.AddTypeToKeep(collection.Name);

eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Mgmt/src/Providers/ResourceClientProvider.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ internal class ResourceClientProvider : TypeProvider
3333
{
3434
private IReadOnlyCollection<InputServiceMethod> _resourceServiceMethods;
3535
private readonly IReadOnlyList<string> _contextualParameters;
36-
private bool _isSingleton;
3736

3837
private FieldProvider _dataField;
3938
private FieldProvider _resourcetypeField;
@@ -45,7 +44,7 @@ public ResourceClientProvider(InputClient inputClient)
4544
{
4645
var resourceMetadata = inputClient.Decorators.Single(d => d.Name.Equals(KnownDecorators.ResourceMetadata));
4746
var codeModelId = resourceMetadata.Arguments?[KnownDecorators.ResourceModel].ToObjectFromJson<string>()!;
48-
_isSingleton = resourceMetadata.Arguments?.TryGetValue("isSingleton", out var isSingleton) == true ? isSingleton.ToObjectFromJson<string>() == "true" : false;
47+
IsSingleton = resourceMetadata.Arguments?.TryGetValue("isSingleton", out var isSingleton) == true ? isSingleton.ToObjectFromJson<bool>() : false;
4948
var resourceType = resourceMetadata.Arguments?[KnownDecorators.ResourceType].ToObjectFromJson<string>()!;
5049
_resourcetypeField = new FieldProvider(FieldModifiers.Public | FieldModifiers.Static | FieldModifiers.ReadOnly, typeof(ResourceType), "ResourceType", this, description: $"Gets the resource type for the operations.", initializationValue: Literal(resourceType));
5150
var resourceModel = ManagementClientGenerator.Instance.InputLibrary.GetModelByCrossLanguageDefinitionId(codeModelId)!;
@@ -86,6 +85,8 @@ private IReadOnlyList<string> GetContextualParameters(string contextualRequestPa
8685
internal ModelProvider ResourceData { get; }
8786
internal string SpecName { get; }
8887

88+
public bool IsSingleton { get; }
89+
8990
protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", $"{Name}.cs");
9091

9192
protected override FieldProvider[] BuildFields() => [_dataField, _clientDiagonosticsField, _restClientField, _resourcetypeField];
@@ -214,7 +215,7 @@ protected override MethodProvider[] BuildMethods()
214215
}
215216

216217
// only update for non-singleton resource
217-
var isUpdateOnly = method.Operation.HttpMethod == HttpMethod.Put.ToString() && !_isSingleton;
218+
var isUpdateOnly = method.Operation.HttpMethod == HttpMethod.Put.ToString() && !IsSingleton;
218219
operationMethods.Add(BuildOperationMethod(method, convenienceMethod, false, isUpdateOnly));
219220
var asyncConvenienceMethod = GetCorrespondingConvenienceMethod(method.Operation, true);
220221
operationMethods.Add(BuildOperationMethod(method, asyncConvenienceMethod, true, isUpdateOnly));

eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/foo.tsp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface Foos {
3737
createOrUpdate is ArmResourceCreateOrUpdateAsync<Foo>;
3838

3939
get is ArmResourceRead<Foo>;
40-
40+
4141
delete is ArmResourceDeleteWithoutOkAsync<Foo>;
4242

4343
list is ArmResourceListByParent<Foo>;

0 commit comments

Comments
 (0)