Skip to content

Commit 3df3682

Browse files
authored
Refine resource detection (Azure#50936)
1 parent 8ececba commit 3df3682

File tree

62 files changed

+2258
-1072
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2258
-1072
lines changed

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { EmitContext } from "@typespec/compiler";
55

6-
import { CodeModel } from "@typespec/http-client-csharp";
6+
import { CodeModel, CSharpEmitterContext } from "@typespec/http-client-csharp";
77

88
import {
99
$onEmit as $onAzureEmit,
@@ -19,10 +19,12 @@ export async function $onEmit(context: EmitContext<AzureEmitterOptions>) {
1919
context.options["sdk-context-options"] ??= azureSDKContextOptions;
2020
context.options["model-namespace"] ??= true;
2121
await $onAzureEmit(context);
22-
}
23-
24-
function updateCodeModel(codeModel: CodeModel): CodeModel {
25-
updateClients(codeModel);
2622

27-
return codeModel;
23+
function updateCodeModel(
24+
codeModel: CodeModel,
25+
sdkContext: CSharpEmitterContext
26+
): CodeModel {
27+
updateClients(codeModel, sdkContext);
28+
return codeModel;
29+
}
2830
}

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

Lines changed: 198 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,196 @@
33

44
import {
55
CodeModel,
6+
CSharpEmitterContext,
67
InputClient,
78
InputModelType
89
} from "@typespec/http-client-csharp";
910
import {
1011
calculateResourceTypeFromPath,
1112
ResourceMetadata,
13+
ResourceOperationKind,
1214
ResourceScope
1315
} from "./resource-metadata.js";
14-
import { DecoratorInfo } from "@azure-tools/typespec-client-generator-core";
1516
import {
16-
armResourceCreateOrUpdate,
17-
armResourceOperations,
18-
armResourceRead,
17+
DecoratorInfo,
18+
getClientType,
19+
SdkClientType,
20+
SdkContext,
21+
SdkHttpOperation,
22+
SdkMethod,
23+
SdkModelType,
24+
SdkServiceOperation
25+
} from "@azure-tools/typespec-client-generator-core";
26+
import {
27+
armResourceActionName,
28+
armResourceCreateOrUpdateName,
29+
armResourceDeleteName,
30+
armResourceInternal,
31+
armResourceListName,
32+
armResourceReadName,
33+
armResourceUpdateName,
34+
parentResourceName,
1935
resourceGroupResource,
2036
resourceMetadata,
2137
singleton,
2238
subscriptionResource,
2339
tenantResource
2440
} from "./sdk-context-options.js";
41+
import { DecoratorApplication, Model, NoTarget } from "@typespec/compiler";
42+
import { AzureEmitterOptions } from "@azure-typespec/http-client-csharp";
43+
44+
export async function updateClients(
45+
codeModel: CodeModel,
46+
sdkContext: CSharpEmitterContext
47+
) {
48+
const serviceMethods = new Map<string, SdkMethod<SdkHttpOperation>>(
49+
getAllSdkClients(sdkContext)
50+
.flatMap((c) => c.methods)
51+
.map((obj) => [obj.crossLanguageDefinitionId, obj])
52+
);
53+
const models = new Map<string, SdkModelType>(
54+
sdkContext.sdkPackage.models.map((m) => [m.crossLanguageDefinitionId, m])
55+
);
56+
const resourceModels = getAllResourceModels(codeModel);
57+
58+
const resourceModelMap = new Map<string, ResourceMetadata>(
59+
resourceModels.map((m) => [
60+
m.crossLanguageDefinitionId,
61+
{
62+
resourceType: "",
63+
isSingleton: m.decorators?.some((d) => d.name == singleton) ?? false,
64+
resourceScope: getResourceScope(m),
65+
methods: [],
66+
parentResource: getParentResourceModelId(
67+
sdkContext,
68+
models.get(m.crossLanguageDefinitionId)
69+
)
70+
} as ResourceMetadata
71+
])
72+
);
2573

26-
export function updateClients(codeModel: CodeModel) {
2774
// first we flatten all possible clients in the code model
2875
const clients = getAllClients(codeModel);
2976

30-
// to fully calculation the resource metadata, we have to go with 2 passes
31-
// in which the first pass we gather everything we could for each client
32-
// the second pass we figure out there cross references between the clients (such as parent resource)
33-
// then pass to update all the clients with their own information
34-
const metadata: Map<InputClient, ResourceMetadata> = new Map();
77+
// then we iterate over all the clients and their methods to find the resource operations
78+
// and add them to the resource model metadata
79+
// we also calculate the resource type from the path of the operation
3580
for (const client of clients) {
36-
gatherResourceMetadata(client, metadata);
81+
for (const method of client.methods) {
82+
const serviceMethod = serviceMethods.get(
83+
method.crossLanguageDefinitionId
84+
);
85+
const [kind, modelId] =
86+
parseResourceOperation(serviceMethod, sdkContext) ?? [];
87+
if (modelId && kind) {
88+
const entry = resourceModelMap.get(modelId);
89+
entry?.methods.push({
90+
id: method.crossLanguageDefinitionId,
91+
kind
92+
});
93+
if (entry && !entry.resourceType) {
94+
entry.resourceType = calculateResourceTypeFromPath(
95+
method.operation.path
96+
);
97+
}
98+
}
99+
}
37100
}
38101

39-
// populate the parent resource information
102+
// the last step, add the decorator to the resource model
103+
for (const model of resourceModels) {
104+
const metadata = resourceModelMap.get(model.crossLanguageDefinitionId);
105+
if (metadata) {
106+
addResourceMetadata(model, metadata);
107+
}
108+
}
109+
}
40110

41-
// the last step, add the decorator to the client
42-
for (const client of clients) {
43-
const resourceMetadata = metadata.get(client);
44-
if (resourceMetadata) {
45-
addResourceMetadata(client, resourceMetadata);
111+
function parseResourceOperation(
112+
serviceMethod: SdkMethod<SdkHttpOperation> | undefined,
113+
sdkContext: CSharpEmitterContext
114+
): [ResourceOperationKind, string | undefined] | undefined {
115+
const decorators = serviceMethod?.__raw?.decorators;
116+
for (const decorator of decorators ?? []) {
117+
if (decorator.definition?.name === armResourceReadName) {
118+
return [
119+
ResourceOperationKind.Get,
120+
getResourceModelId(sdkContext, decorator)
121+
];
122+
} else if (decorator.definition?.name == armResourceCreateOrUpdateName) {
123+
return [
124+
ResourceOperationKind.Create,
125+
getResourceModelId(sdkContext, decorator)
126+
];
127+
} else if (decorator.definition?.name == armResourceUpdateName) {
128+
return [
129+
ResourceOperationKind.Update,
130+
getResourceModelId(sdkContext, decorator)
131+
];
132+
} else if (decorator.definition?.name == armResourceDeleteName) {
133+
return [
134+
ResourceOperationKind.Delete,
135+
getResourceModelId(sdkContext, decorator)
136+
];
137+
} else if (decorator.definition?.name == armResourceListName) {
138+
return [
139+
ResourceOperationKind.List,
140+
getResourceModelId(sdkContext, decorator)
141+
];
142+
} else if (decorator.definition?.name == armResourceActionName) {
143+
return [
144+
ResourceOperationKind.Action,
145+
getResourceModelId(sdkContext, decorator)
146+
];
46147
}
47148
}
149+
return undefined;
48150
}
49151

50-
export function getAllClients(codeModel: CodeModel): InputClient[] {
51-
const clients: InputClient[] = [];
52-
for (const client of codeModel.clients) {
152+
function getParentResourceModelId(
153+
sdkContext: CSharpEmitterContext,
154+
model: SdkModelType | undefined
155+
): string | undefined {
156+
const decorators = (model?.__raw as Model)?.decorators;
157+
const parentResourceDecorator = decorators?.find(
158+
(d) => d.definition?.name == parentResourceName
159+
);
160+
return getResourceModelId(sdkContext, parentResourceDecorator) ?? undefined;
161+
}
162+
163+
function getResourceModelId(
164+
sdkContext: CSharpEmitterContext,
165+
decorator?: DecoratorApplication
166+
): string | undefined {
167+
if (!decorator) return undefined;
168+
const model = getClientType(
169+
sdkContext,
170+
decorator.args[0].value as Model
171+
) as SdkModelType;
172+
if (model) {
173+
return model.crossLanguageDefinitionId;
174+
} else {
175+
sdkContext.logger.reportDiagnostic({
176+
code: "general-error",
177+
message: `Resource model not found for decorator ${decorator.decorator.name}`,
178+
target: NoTarget,
179+
severity: "error"
180+
});
181+
return undefined;
182+
}
183+
}
184+
185+
export function getAllSdkClients(
186+
sdkContext: SdkContext<AzureEmitterOptions, SdkHttpOperation>
187+
): SdkClientType<SdkServiceOperation>[] {
188+
const clients: SdkClientType<SdkServiceOperation>[] = [];
189+
for (const client of sdkContext.sdkPackage.clients) {
53190
traverseClient(client);
54191
}
55192

56193
return clients;
57194

58-
function traverseClient(client: InputClient) {
195+
function traverseClient(client: SdkClientType<SdkServiceOperation>) {
59196
clients.push(client);
60197
if (client.children) {
61198
for (const child of client.children) {
@@ -65,97 +202,65 @@ export function getAllClients(codeModel: CodeModel): InputClient[] {
65202
}
66203
}
67204

68-
function gatherResourceMetadata(
69-
client: InputClient,
70-
metadataMap: Map<InputClient, ResourceMetadata>
71-
) {
72-
// TODO: we can implement this decorator in TCGC until we meet the corner case
73-
// if the client has resourceMetadata decorator, it is a resource client and we don't need to add it again
74-
if (client.decorators?.some((d) => d.name == resourceMetadata)) {
75-
return;
205+
export function getAllClients(codeModel: CodeModel): InputClient[] {
206+
const clients: InputClient[] = [];
207+
for (const client of codeModel.clients) {
208+
traverseClient(client);
76209
}
77210

78-
// TODO: Once we have the ability to get resource hierarchy from TCGC directly, we can remove this implementation
79-
// A resource client should have decorator armResourceOperations and contains either a get operation(containing armResourceRead deocrator) or a put operation(containing armResourceCreateOrUpdate decorator)
80-
if (
81-
client.decorators?.some((d) => d.name == armResourceOperations) &&
82-
client.methods.some(
83-
(m) =>
84-
m.operation.decorators?.some(
85-
(d) => d.name == armResourceRead || armResourceCreateOrUpdate
86-
)
87-
)
88-
) {
89-
let resourceModel: InputModelType | undefined = undefined;
90-
let isSingleton: boolean = false;
91-
let resourceType: string | undefined = undefined;
92-
// We will try to get resource metadata from put operation firstly, if not found, we will try to get it from get operation
93-
const putOperation = client.methods.find(
94-
(m) =>
95-
m.operation.decorators?.some((d) => d.name == armResourceCreateOrUpdate)
96-
)?.operation;
97-
if (putOperation) {
98-
const path = putOperation.path;
99-
resourceType = calculateResourceTypeFromPath(path);
100-
resourceModel = putOperation.responses.filter((r) => r.bodyType)[0]
101-
.bodyType as InputModelType;
102-
isSingleton =
103-
resourceModel.decorators?.some((d) => d.name == singleton) ?? false;
104-
} else {
105-
const getOperation = client.methods.find(
106-
(m) => m.operation.decorators?.some((d) => d.name == armResourceRead)
107-
)?.operation;
108-
if (getOperation) {
109-
const path = getOperation.path;
110-
resourceType = calculateResourceTypeFromPath(path);
111-
resourceModel = getOperation.responses.filter((r) => r.bodyType)[0]
112-
.bodyType as InputModelType;
113-
isSingleton =
114-
resourceModel.decorators?.some((d) => d.name == singleton) ?? false;
211+
return clients;
212+
213+
function traverseClient(client: InputClient) {
214+
clients.push(client);
215+
if (client.children) {
216+
for (const child of client.children) {
217+
traverseClient(child);
115218
}
116219
}
220+
}
221+
}
117222

118-
if (resourceModel && resourceType) {
119-
// find the scope on its model
120-
const metadata: ResourceMetadata = {
121-
resourceModel: resourceModel,
122-
resourceClient: client,
123-
resourceType: resourceType,
124-
isSingleton: isSingleton,
125-
resourceScope: getResourceScope(resourceModel)
126-
};
127-
metadataMap.set(client, metadata);
223+
function getAllResourceModels(codeModel: CodeModel): InputModelType[] {
224+
const resourceModels: InputModelType[] = [];
225+
for (const model of codeModel.models) {
226+
if (model.decorators?.some((d) => d.name == armResourceInternal)) {
227+
model.crossLanguageDefinitionId;
228+
resourceModels.push(model);
128229
}
129230
}
231+
return resourceModels;
232+
}
130233

131-
function getResourceScope(model: InputModelType): ResourceScope {
132-
const decorators = model.decorators;
133-
if (decorators?.some((d) => d.name == tenantResource)) {
134-
return ResourceScope.Tenant;
135-
} else if (decorators?.some((d) => d.name == subscriptionResource)) {
136-
return ResourceScope.Subscription;
137-
} else if (decorators?.some((d) => d.name == resourceGroupResource)) {
138-
return ResourceScope.ResourceGroup;
139-
}
140-
return ResourceScope.ResourceGroup; // all the templates work as if there is a resource group decorator when there is no such decorator
234+
function getResourceScope(model: InputModelType): ResourceScope {
235+
const decorators = model.decorators;
236+
if (decorators?.some((d) => d.name == tenantResource)) {
237+
return ResourceScope.Tenant;
238+
} else if (decorators?.some((d) => d.name == subscriptionResource)) {
239+
return ResourceScope.Subscription;
240+
} else if (decorators?.some((d) => d.name == resourceGroupResource)) {
241+
return ResourceScope.ResourceGroup;
141242
}
243+
return ResourceScope.ResourceGroup; // all the templates work as if there is a resource group decorator when there is no such decorator
142244
}
143245

144-
function addResourceMetadata(client: InputClient, metadata: ResourceMetadata) {
246+
function addResourceMetadata(
247+
model: InputModelType,
248+
metadata: ResourceMetadata
249+
) {
145250
const resourceMetadataDecorator: DecoratorInfo = {
146251
name: resourceMetadata,
147252
arguments: {
148-
resourceModel: metadata.resourceModel.crossLanguageDefinitionId,
149-
resourceClient: metadata.resourceClient.crossLanguageDefinitionId,
150253
isSingleton: metadata.isSingleton,
151254
resourceType: metadata.resourceType,
152-
resourceScope: metadata.resourceScope
255+
resourceScope: metadata.resourceScope,
256+
methods: metadata.methods,
257+
parentResource: metadata.parentResource
153258
}
154259
};
155260

156-
if (!client.decorators) {
157-
client.decorators = [];
261+
if (!model.decorators) {
262+
model.decorators = [];
158263
}
159264

160-
client.decorators.push(resourceMetadataDecorator);
265+
model.decorators.push(resourceMetadataDecorator);
161266
}

0 commit comments

Comments
 (0)