Skip to content

Commit 124feb7

Browse files
authored
Implement mgmt resource (Azure#47944)
1 parent 3fbe812 commit 124feb7

File tree

64 files changed

+2509
-2464
lines changed

Some content is hidden

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

64 files changed

+2509
-2464
lines changed

eng/Packages.Data.props

+1
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@
300300
<PackageReference Update="Azure.Messaging.EventGrid" Version="4.17.0" />
301301
<PackageReference Update="Azure.Messaging.EventHubs.Processor" Version="5.11.6" />
302302
<PackageReference Update="Azure.Messaging.ServiceBus" Version="7.18.2" />
303+
<PackageReference Update="Azure.ResourceManager" Version="1.13.0" />
303304
<PackageReference Update="Azure.ResourceManager.Compute" Version="1.7.0-beta.1" />
304305
<PackageReference Update="Azure.ResourceManager.CognitiveServices" Version="1.3.0" />
305306
<PackageReference Update="Azure.ResourceManager.KeyVault" Version="1.1.0" />

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

+76-2
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,91 @@
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";
56

67
import {
7-
$onEmit as $OnMGCEmit,
8+
$onEmit as $onMTGEmit,
9+
CodeModel,
810
CSharpEmitterOptions,
11+
InputModelType,
912
setSDKContextOptions
1013
} from "@typespec/http-client-csharp";
1114
import { azureSDKContextOptions } from "./sdk-context-options.js";
15+
import { calculateResourceTypeFromPath } from "./resource-type.js";
16+
17+
const armResourceOperations = "Azure.ResourceManager.@armResourceOperations";
18+
const armResourceRead = "Azure.ResourceManager.@armResourceRead";
19+
const armResourceCreateOrUpdate =
20+
"Azure.ResourceManager.@armResourceCreateOrUpdate";
21+
const singleton = "Azure.ResourceManager.@singleton";
22+
const resourceMetadata = "Azure.ClientGenerator.Core.@resourceSchema";
1223

1324
export async function $onEmit(context: EmitContext<CSharpEmitterOptions>) {
1425
context.options["plugin-name"] ??= "AzureClientPlugin";
1526
context.options["emitter-extension-path"] = import.meta.url;
27+
context.options["update-code-model"] = updateCodeModel;
1628
setSDKContextOptions(azureSDKContextOptions);
17-
await $OnMGCEmit(context);
29+
await $onMTGEmit(context);
30+
}
31+
32+
function updateCodeModel(codeModel: CodeModel): CodeModel {
33+
for (const client of codeModel.Clients) {
34+
// TODO: we can implement this decorator in TCGC until we meet the corner case
35+
// if the client has resourceMetadata decorator, it is a resource client and we don't need to add it again
36+
if (client.Decorators?.some((d) => d.name == resourceMetadata)) {
37+
continue;
38+
}
39+
40+
// TODO: Once we have the ability to get resource hierarchy from TCGC directly, we can remove this implementation
41+
// A resource client should have decorator armResourceOperations and contains either a get operation(containing armResourceRead deocrator) or a put operation(containing armResourceCreateOrUpdate decorator)
42+
if (
43+
client.Decorators?.some((d) => d.name == armResourceOperations) &&
44+
client.Operations.some(
45+
(op) =>
46+
op.Decorators?.some(
47+
(d) => d.name == armResourceRead || armResourceCreateOrUpdate
48+
)
49+
)
50+
) {
51+
let resourceModel: InputModelType | undefined = undefined;
52+
let isSingleton: boolean = false;
53+
let resourceType: string | undefined = undefined;
54+
// We will try to get resource metadata from put operation firstly, if not found, we will try to get it from get operation
55+
const putOperation = client.Operations.find(
56+
(op) => op.Decorators?.some((d) => d.name == armResourceCreateOrUpdate)
57+
);
58+
if (putOperation) {
59+
const path = putOperation.Path;
60+
resourceType = calculateResourceTypeFromPath(path);
61+
resourceModel = putOperation.Responses.filter((r) => r.BodyType)[0]
62+
.BodyType as InputModelType;
63+
isSingleton =
64+
resourceModel.decorators?.some((d) => d.name == singleton) ?? false;
65+
} else {
66+
const getOperation = client.Operations.find(
67+
(op) => op.Decorators?.some((d) => d.name == armResourceRead)
68+
);
69+
if (getOperation) {
70+
const path = getOperation.Path;
71+
resourceType = calculateResourceTypeFromPath(path);
72+
resourceModel = getOperation.Responses.filter((r) => r.BodyType)[0]
73+
.BodyType as InputModelType;
74+
isSingleton =
75+
resourceModel.decorators?.some((d) => d.name == singleton) ?? false;
76+
}
77+
}
78+
79+
const resourceMetadataDecorator: DecoratorInfo = {
80+
name: resourceMetadata,
81+
arguments: {}
82+
};
83+
resourceMetadataDecorator.arguments["resourceModel"] =
84+
resourceModel?.crossLanguageDefinitionId;
85+
resourceMetadataDecorator.arguments["isSingleton"] =
86+
isSingleton.toString();
87+
resourceMetadataDecorator.arguments["resourceType"] = resourceType;
88+
client.Decorators.push(resourceMetadataDecorator);
89+
}
90+
}
91+
return codeModel;
1892
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const ResourceGroupScopePrefix =
2+
"/subscriptions/{subscriptionId}/resourceGroups";
3+
const SubscriptionScopePrefix = "/subscriptions";
4+
const TenantScopePrefix = "/tenants";
5+
const Providers = "/providers";
6+
7+
export function calculateResourceTypeFromPath(path: string): string {
8+
const providerIndex = path.indexOf(Providers);
9+
if (providerIndex === -1) {
10+
if (path.startsWith(ResourceGroupScopePrefix)) {
11+
return "Microsoft.Resources/resourceGroups";
12+
} else if (path.startsWith(SubscriptionScopePrefix)) {
13+
return "Microsoft.Resources/subscriptions";
14+
} else if (path.startsWith(TenantScopePrefix)) {
15+
return "Microsoft.Resources/tenants";
16+
}
17+
throw `Path ${path} doesn't have resource type`;
18+
}
19+
20+
return path
21+
.substring(providerIndex + Providers.length)
22+
.split("/")
23+
.reduce((result, current, index) => {
24+
if (index === 1 || index % 2 === 0)
25+
return result === "" ? current : `${result}/${current}`;
26+
else return result;
27+
}, "");
28+
}

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

+18-7
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@
44
import { CreateSdkContextOptions } from "@azure-tools/typespec-client-generator-core";
55

66
export const azureSDKContextOptions: CreateSdkContextOptions = {
7-
versioning: {},
8-
additionalDecorators: [
9-
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-client-generator-core/README.md#usesystemtextjsonconverter
10-
"Azure\\.ClientGenerator\\.Core\\.@useSystemTextJsonConverter",
11-
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armprovidernamespace
12-
"Azure\\.ResourceManager\\.@armProviderNamespace"
13-
]
7+
versioning: {},
8+
additionalDecorators: [
9+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-client-generator-core/README.md#usesystemtextjsonconverter
10+
"Azure\\.ClientGenerator\\.Core\\.@useSystemTextJsonConverter",
11+
// TODO: add this decorator to TCGC
12+
"Azure\\.ClientGenerator\\.Core\\.@resourceSchema",
13+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armprovidernamespace
14+
"Azure\\.ResourceManager\\.@armProviderNamespace",
15+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armresourceoperations
16+
"Azure\\.ResourceManager\\.@armResourceOperations",
17+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#armResourceRead
18+
"Azure\\.ResourceManager\\.@armResourceRead",
19+
// https://github.com/microsoft/typespec/blob/main/packages/rest/README.md#parentresource
20+
"TypeSpec\\.Rest\\.parentResource",
21+
// https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/README.md#singleton
22+
"Azure\\.ResourceManager\\.@singleton",
23+
"Azure\\.ResourceManager\\.Private\\.@armResourceInternal"
24+
]
1425
};

eng/packages/http-client-csharp/emitter/test/Unit/property-type.test.ts

+80-57
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ import { UsageFlags } from "@azure-tools/typespec-client-generator-core";
33
import { strictEqual } from "assert";
44
import { beforeEach, describe, it } from "vitest";
55
import { createModel } from "@typespec/http-client-csharp";
6-
import { createCSharpSdkContext, createEmitterContext, createEmitterTestHost, typeSpecCompile, } from "./test-util.js";
6+
import {
7+
createCSharpSdkContext,
8+
createEmitterContext,
9+
createEmitterTestHost,
10+
typeSpecCompile
11+
} from "./test-util.js";
712

813
describe("Test GetInputType for enum", () => {
9-
let runner: TestHost;
10-
beforeEach(async () => {
11-
runner = await createEmitterTestHost();
12-
});
13-
it("Fixed string enum", async () => {
14-
const program = await typeSpecCompile(`
14+
let runner: TestHost;
15+
beforeEach(async () => {
16+
runner = await createEmitterTestHost();
17+
});
18+
it("Fixed string enum", async () => {
19+
const program = await typeSpecCompile(
20+
`
1521
#suppress "@azure-tools/typespec-azure-core/use-extensible-enum" "Enums should be defined without the @fixed decorator."
1622
@doc("fixed string enum")
1723
@fixed
@@ -26,31 +32,40 @@ describe("Test GetInputType for enum", () => {
2632
#suppress "@azure-tools/typespec-azure-core/use-standard-operations" "Operation 'test' should be defined using a signature from the Azure.Core namespace."
2733
@doc("test fixed enum.")
2834
op test(@doc("fixed enum as input.")@body input: SimpleEnum): string[];
29-
`, runner, { IsNamespaceNeeded: true});
30-
const context = createEmitterContext(program);
31-
const sdkContext = await createCSharpSdkContext(context);
32-
const root = createModel(sdkContext);
33-
const inputParamArray = root.Clients[0].Operations[0].Parameters.filter((p) => p.Name === "input");
34-
strictEqual(1, inputParamArray.length);
35-
const type = inputParamArray[0].Type;
36-
strictEqual(type.kind, "enum");
37-
strictEqual(type.name, "SimpleEnum");
38-
strictEqual(type.isFixed, true);
39-
strictEqual(type.doc, "fixed string enum");
40-
strictEqual(type.crossLanguageDefinitionId, "Azure.Csharp.Testing.SimpleEnum");
41-
strictEqual(type.access, undefined);
42-
strictEqual(type.valueType.kind, "string");
43-
strictEqual(type.values.length, 3);
44-
strictEqual(type.values[0].name, "One");
45-
strictEqual(type.values[0].value, "1");
46-
strictEqual(type.values[1].name, "Two");
47-
strictEqual(type.values[1].value, "2");
48-
strictEqual(type.values[2].name, "Four");
49-
strictEqual(type.values[2].value, "4");
50-
strictEqual(type.usage, UsageFlags.Input | UsageFlags.Json);
51-
});
52-
it("Fixed int enum", async () => {
53-
const program = await typeSpecCompile(`
35+
`,
36+
runner,
37+
{ IsNamespaceNeeded: true }
38+
);
39+
const context = createEmitterContext(program);
40+
const sdkContext = await createCSharpSdkContext(context);
41+
const root = createModel(sdkContext);
42+
const inputParamArray = root.Clients[0].Operations[0].Parameters.filter(
43+
(p) => p.Name === "input"
44+
);
45+
strictEqual(1, inputParamArray.length);
46+
const type = inputParamArray[0].Type;
47+
strictEqual(type.kind, "enum");
48+
strictEqual(type.name, "SimpleEnum");
49+
strictEqual(type.isFixed, true);
50+
strictEqual(type.doc, "fixed string enum");
51+
strictEqual(
52+
type.crossLanguageDefinitionId,
53+
"Azure.Csharp.Testing.SimpleEnum"
54+
);
55+
strictEqual(type.access, undefined);
56+
strictEqual(type.valueType.kind, "string");
57+
strictEqual(type.values.length, 3);
58+
strictEqual(type.values[0].name, "One");
59+
strictEqual(type.values[0].value, "1");
60+
strictEqual(type.values[1].name, "Two");
61+
strictEqual(type.values[1].value, "2");
62+
strictEqual(type.values[2].name, "Four");
63+
strictEqual(type.values[2].value, "4");
64+
strictEqual(type.usage, UsageFlags.Input | UsageFlags.Json);
65+
});
66+
it("Fixed int enum", async () => {
67+
const program = await typeSpecCompile(
68+
`
5469
#suppress "@azure-tools/typespec-azure-core/use-extensible-enum" "Enums should be defined without the @fixed decorator."
5570
@doc("Fixed int enum")
5671
@fixed
@@ -65,29 +80,37 @@ describe("Test GetInputType for enum", () => {
6580
#suppress "@azure-tools/typespec-azure-core/use-standard-operations" "Operation 'test' should be defined using a signature from the Azure.Core namespace."
6681
@doc("test fixed enum.")
6782
op test(@doc("fixed enum as input.")@body input: FixedIntEnum): string[];
68-
`, runner, { IsNamespaceNeeded: true });
69-
const context = createEmitterContext(program);
70-
const sdkContext = await createCSharpSdkContext(context);
71-
const root = createModel(sdkContext);
72-
const inputParamArray = root.Clients[0].Operations[0].Parameters.filter((p) => p.Name === "input");
73-
strictEqual(1, inputParamArray.length);
74-
const type = inputParamArray[0].Type;
75-
strictEqual(type.kind, "enum");
76-
strictEqual(type.name, "FixedIntEnum");
77-
strictEqual(type.crossLanguageDefinitionId, "Azure.Csharp.Testing.FixedIntEnum");
78-
strictEqual(type.access, undefined);
79-
strictEqual(type.doc, "Fixed int enum");
80-
strictEqual(type.valueType.crossLanguageDefinitionId, "TypeSpec.int32");
81-
strictEqual(type.valueType.kind, "int32");
82-
strictEqual(type.values.length, 3);
83-
strictEqual(type.values[0].name, "One");
84-
strictEqual(type.values[0].value, 1);
85-
strictEqual(type.values[1].name, "Two");
86-
strictEqual(type.values[1].value, 2);
87-
strictEqual(type.values[2].name, "Four");
88-
strictEqual(type.values[2].value, 4);
89-
strictEqual(type.isFixed, true);
90-
strictEqual(type.usage, UsageFlags.Input | UsageFlags.Json);
91-
});
83+
`,
84+
runner,
85+
{ IsNamespaceNeeded: true }
86+
);
87+
const context = createEmitterContext(program);
88+
const sdkContext = await createCSharpSdkContext(context);
89+
const root = createModel(sdkContext);
90+
const inputParamArray = root.Clients[0].Operations[0].Parameters.filter(
91+
(p) => p.Name === "input"
92+
);
93+
strictEqual(1, inputParamArray.length);
94+
const type = inputParamArray[0].Type;
95+
strictEqual(type.kind, "enum");
96+
strictEqual(type.name, "FixedIntEnum");
97+
strictEqual(
98+
type.crossLanguageDefinitionId,
99+
"Azure.Csharp.Testing.FixedIntEnum"
100+
);
101+
strictEqual(type.access, undefined);
102+
strictEqual(type.doc, "Fixed int enum");
103+
strictEqual(type.valueType.crossLanguageDefinitionId, "TypeSpec.int32");
104+
strictEqual(type.valueType.kind, "int32");
105+
strictEqual(type.values.length, 3);
106+
strictEqual(type.values[0].name, "One");
107+
strictEqual(type.values[0].value, 1);
108+
strictEqual(type.values[1].name, "Two");
109+
strictEqual(type.values[1].value, 2);
110+
strictEqual(type.values[2].name, "Four");
111+
strictEqual(type.values[2].value, 4);
112+
strictEqual(type.isFixed, true);
113+
strictEqual(type.usage, UsageFlags.Input | UsageFlags.Json);
114+
});
92115
});
93-
//# sourceMappingURL=property-type.test.js.map
116+
//# sourceMappingURL=property-type.test.js.map

eng/packages/http-client-csharp/emitter/test/Unit/scalar.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
createCSharpSdkContext,
77
createEmitterContext,
88
createEmitterTestHost,
9-
typeSpecCompile,
9+
typeSpecCompile
1010
} from "./test-util.js";
1111

1212
describe("Test GetInputType for scalar", () => {
@@ -21,13 +21,13 @@ describe("Test GetInputType for scalar", () => {
2121
`
2222
op test(@query location: azureLocation): void;
2323
`,
24-
runner,
24+
runner
2525
);
2626
const context = await createCSharpSdkContext(createEmitterContext(program));
2727
const model = createModel(context);
2828

2929
const inputParamArray = model.Clients[0].Operations[0].Parameters.filter(
30-
(p) => p.Name === "location",
30+
(p) => p.Name === "location"
3131
);
3232
strictEqual(1, inputParamArray.length);
3333
const type = inputParamArray[0].Type;

0 commit comments

Comments
 (0)