From 62b2766a167b6c569df46dfdb6b0b15020a06cfa Mon Sep 17 00:00:00 2001 From: Arcturus Zhang Date: Tue, 27 May 2025 17:08:17 +0800 Subject: [PATCH 1/5] update --- .../src/ManagementOutputLibrary.cs | 53 ++++++++++++++++--- .../src/ManagementTypeFactory.cs | 18 +++++++ .../src/Providers/MockableResourceProvider.cs | 30 +++++++++++ .../src/Providers/ResourceClientProvider.cs | 35 ++++++++---- .../ResourceCollectionClientProvider.cs | 8 +-- ...ckableMgmtTypeSpecResourceGroupResource.cs | 16 ++++++ 6 files changed, 142 insertions(+), 18 deletions(-) create mode 100644 eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/MockableResourceProvider.cs create mode 100644 eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MockableMgmtTypeSpecResourceGroupResource.cs diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementOutputLibrary.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementOutputLibrary.cs index f539301b9b48..c5b94ed98f2b 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementOutputLibrary.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementOutputLibrary.cs @@ -1,8 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Azure.Generator.Management.Models; using Azure.Generator.Management.Providers; +using Azure.ResourceManager.Resources; +using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Providers; +using System; using System.Collections.Generic; using System.Linq; @@ -28,20 +32,55 @@ public class ManagementOutputLibrary : AzureOutputLibrary return (resources, collections); } - private static void BuildResourceCore(List resources, List collections, Microsoft.TypeSpec.Generator.Input.InputClient client) + private static readonly IReadOnlyDictionary _scopeToTypes = new Dictionary + { + [ResourceScope.ResourceGroup] = typeof(ResourceGroupResource), + [ResourceScope.Subscription] = typeof(SubscriptionResource), + [ResourceScope.Tenant] = typeof(TenantResource), + }; + + // TODO -- build extensions and their corresponding mockable resources + private IReadOnlyList BuildResourceExtensions(IEnumerable resources) + { + // walk through all resources to figure out their scopes + var scopeCandidates = new Dictionary> + { + [ResourceScope.ResourceGroup] = [], + [ResourceScope.Subscription] = [], + [ResourceScope.Tenant] = [], + }; + foreach (var resource in resources) + { + scopeCandidates[resource.ResourceScope].Add(resource); + } + + var mockableResources = new List(scopeCandidates.Count); + foreach (var (scope, candidates) in scopeCandidates) + { + if (candidates.Count > 0) + { + var mockableExtension = new MockableResourceProvider(_scopeToTypes[scope], candidates); + ManagementClientGenerator.Instance.AddTypeToKeep(mockableExtension.Name); + mockableResources.Add(mockableExtension); + } + } + + return mockableResources; + } + + private static void BuildResourceCore(List resources, List collections, InputClient client) { // A resource client should contain the decorator "Azure.ResourceManager.@resourceMetadata" var resourceMetadata = ManagementClientGenerator.Instance.InputLibrary.GetResourceMetadata(client); if (resourceMetadata is not null) { - var resource = new ResourceClientProvider(client, resourceMetadata); + var resource = ResourceClientProvider.Create(client, resourceMetadata); ManagementClientGenerator.Instance.AddTypeToKeep(resource.Name); resources.Add(resource); - if (!resource.IsSingleton) + if (resource.ResourceCollection is not null) { - var collection = new ResourceCollectionClientProvider(client, resourceMetadata, resource); - ManagementClientGenerator.Instance.AddTypeToKeep(collection.Name); - collections.Add(collection); + ManagementClientGenerator.Instance.AddTypeToKeep(resource.ResourceCollection.Name); + collections.Add(resource.ResourceCollection); } } } @@ -50,12 +89,14 @@ private static void BuildResourceCore(List resources, Li protected override TypeProvider[] BuildTypeProviders() { var (resources, collections) = BuildResources(); + var extensions = BuildResourceExtensions(resources); return [ .. base.BuildTypeProviders().Where(t => t is not InheritableSystemObjectModelProvider), ArmOperation, GenericArmOperation, .. resources, .. collections, + .. extensions, .. resources.Select(r => r.Source), .. resources.SelectMany(r => r.SerializationProviders)]; } diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementTypeFactory.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementTypeFactory.cs index 8f4bfd850db4..87cd29279d6c 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementTypeFactory.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementTypeFactory.cs @@ -24,6 +24,24 @@ namespace Azure.Generator.Management /// public class ManagementTypeFactory : AzureTypeFactory { + private string? _resourceProviderName; + + /// + /// The name for this resource provider. + /// For instance, if the namespace is "Azure.ResourceManager.Compute", the resource provider name will be "Compute". + /// + public string ResourceProviderName => _resourceProviderName ??= BuildResourceProviderName(); + + private string BuildResourceProviderName() + { + const string armNamespacePrefix = "Azure.ResourceManager."; + if (PrimaryNamespace.StartsWith(armNamespacePrefix)) + { + return PrimaryNamespace[(armNamespacePrefix.Length + 1)..]; // TODO -- we need to call ToCleanName here to trim off invalid characters + } + return PrimaryNamespace; + } + /// public override IClientPipelineApi ClientPipelineApi => MgmtHttpPipelineProvider.Instance; diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/MockableResourceProvider.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/MockableResourceProvider.cs new file mode 100644 index 000000000000..13c42080b48e --- /dev/null +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/MockableResourceProvider.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.ResourceManager; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using System.Collections.Generic; +using System.IO; + +namespace Azure.Generator.Management.Providers +{ + internal class MockableResourceProvider : TypeProvider + { + private readonly CSharpType _armCoreType; + private readonly IReadOnlyList _resources; + + // TODO -- in the future we need to update this to include the operations this mockable resource should include. + public MockableResourceProvider(CSharpType armCoreType, IReadOnlyList resources) + { + _armCoreType = armCoreType; + _resources = resources; + } + + protected override string BuildName() => $"Mockable{ManagementClientGenerator.Instance.TypeFactory.ResourceProviderName}{_armCoreType.Name}"; + + protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Extensions", $"{Name}.cs"); + + protected override CSharpType? GetBaseType() => typeof(ArmResource); + } +} diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceClientProvider.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceClientProvider.cs index 2b92dea88709..1918ab06aa3b 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceClientProvider.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceClientProvider.cs @@ -31,35 +31,52 @@ namespace Azure.Generator.Management.Providers /// internal class ResourceClientProvider : TypeProvider { + public static ResourceClientProvider Create(InputClient inputClient, ResourceMetadata resourceMetadata) + { + var resource = new ResourceClientProvider(inputClient, resourceMetadata); + if (!resource.IsSingleton) + { + var collection = new ResourceCollectionClientProvider(inputClient, resourceMetadata, resource); + resource.ResourceCollection = collection; + } + + return resource; + } + private IReadOnlyCollection _resourceServiceMethods; private FieldProvider _dataField; private FieldProvider _resourceTypeField; - protected ClientProvider _clientProvider; + protected ClientProvider _restClientProvider; protected FieldProvider _clientDiagonosticsField; protected FieldProvider _restClientField; - public ResourceClientProvider(InputClient inputClient, ResourceMetadata resourceMetadata) + private protected ResourceClientProvider(InputClient inputClient, ResourceMetadata resourceMetadata) { IsSingleton = resourceMetadata.IsSingleton; + ResourceScope = resourceMetadata.ResourceScope; var resourceType = resourceMetadata.ResourceType; _resourceTypeField = new FieldProvider(FieldModifiers.Public | FieldModifiers.Static | FieldModifiers.ReadOnly, typeof(ResourceType), "ResourceType", this, description: $"Gets the resource type for the operations.", initializationValue: Literal(resourceType)); var resourceModel = resourceMetadata.ResourceModel; - SpecName = resourceModel.Name; + SpecName = resourceModel.Name; // TODO -- here we need to call the ToCleanName // We should be able to assume that all operations in the resource client are for the same resource var requestPath = new RequestPath(inputClient.Methods.First().Operation.Path); _resourceServiceMethods = inputClient.Methods; ResourceData = ManagementClientGenerator.Instance.TypeFactory.CreateModel(resourceModel)!; - _clientProvider = ManagementClientGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + _restClientProvider = ManagementClientGenerator.Instance.TypeFactory.CreateClient(inputClient)!; ContextualParameters = GetContextualParameters(requestPath); _dataField = new FieldProvider(FieldModifiers.Private | FieldModifiers.ReadOnly, ResourceData.Type, "_data", this); _clientDiagonosticsField = new FieldProvider(FieldModifiers.Private | FieldModifiers.ReadOnly, typeof(ClientDiagnostics), $"_{SpecName.ToLower()}ClientDiagnostics", this); - _restClientField = new FieldProvider(FieldModifiers.Private | FieldModifiers.ReadOnly, _clientProvider.Type, $"_{SpecName.ToLower()}RestClient", this); + _restClientField = new FieldProvider(FieldModifiers.Private | FieldModifiers.ReadOnly, _restClientProvider.Type, $"_{SpecName.ToLower()}RestClient", this); } + internal ResourceScope ResourceScope { get; } + + internal ResourceCollectionClientProvider? ResourceCollection { get; private set; } + private IReadOnlyList GetContextualParameters(string contextualRequestPath) { var contextualParameters = new List(); @@ -168,7 +185,7 @@ protected ConstructorProvider BuildResourceIdentifierConstructor() { _clientDiagonosticsField.Assign(New.Instance(typeof(ClientDiagnostics), Literal(Type.Namespace), ResourceTypeExpression.Property(nameof(ResourceType.Namespace)), This.Property("Diagnostics"))).Terminate(), TryGetApiVersion(out var apiVersion).Terminate(), - _restClientField.Assign(New.Instance(_clientProvider.Type, _clientDiagonosticsField, This.Property("Pipeline"), This.Property("Endpoint"), apiVersion)).Terminate(), + _restClientField.Assign(New.Instance(_restClientProvider.Type, _clientDiagonosticsField, This.Property("Pipeline"), This.Property("Endpoint"), apiVersion)).Terminate(), Static(Type).Invoke(ValidateResourceIdMethodName, idParameter).Terminate() }; @@ -202,7 +219,7 @@ protected MethodProvider BuildValidateResourceIdMethod() protected virtual CSharpType ResourceClientCSharpType => this.Type; - protected override CSharpType[] BuildImplements() => [typeof(ArmResource)]; + protected override CSharpType? GetBaseType() => typeof(ArmResource); protected override MethodProvider[] BuildMethods() { @@ -431,10 +448,10 @@ private ValueExpression[] PopulateArguments(IReadOnlyList par // TODO: get clean name of operation Name protected MethodProvider GetCorrespondingConvenienceMethod(InputOperation operation, bool isAsync) - => _clientProvider.CanonicalView.Methods.Single(m => m.Signature.Name.Equals(isAsync ? $"{operation.Name}Async" : operation.Name, StringComparison.OrdinalIgnoreCase) && m.Signature.Parameters.Any(p => p.Type.Equals(typeof(CancellationToken)))); + => _restClientProvider.CanonicalView.Methods.Single(m => m.Signature.Name.Equals(isAsync ? $"{operation.Name}Async" : operation.Name, StringComparison.OrdinalIgnoreCase) && m.Signature.Parameters.Any(p => p.Type.Equals(typeof(CancellationToken)))); private MethodProvider GetCorrespondingRequestMethod(InputOperation operation) - => _clientProvider.RestClient.Methods.Single(m => m.Signature.Name.Equals($"Create{operation.Name}Request", StringComparison.OrdinalIgnoreCase)); + => _restClientProvider.RestClient.Methods.Single(m => m.Signature.Name.Equals($"Create{operation.Name}Request", StringComparison.OrdinalIgnoreCase)); public ScopedApi TryGetApiVersion(out ScopedApi apiVersion) { diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceCollectionClientProvider.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceCollectionClientProvider.cs index 38d85d72664d..615673552b1a 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceCollectionClientProvider.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceCollectionClientProvider.cs @@ -27,7 +27,7 @@ internal class ResourceCollectionClientProvider : ResourceClientProvider private InputServiceMethod? _create; private InputServiceMethod? _get; - public ResourceCollectionClientProvider(InputClient inputClient, ResourceMetadata resourceMetadata, ResourceClientProvider resource) : base(inputClient, resourceMetadata) + internal ResourceCollectionClientProvider(InputClient inputClient, ResourceMetadata resourceMetadata, ResourceClientProvider resource) : base(inputClient, resourceMetadata) { _resource = resource; @@ -56,10 +56,12 @@ public ResourceCollectionClientProvider(InputClient inputClient, ResourceMetadat protected override string BuildName() => $"{SpecName}Collection"; + protected override CSharpType? GetBaseType() => typeof(ArmCollection); + protected override CSharpType[] BuildImplements() => _getAll is null - ? [typeof(ArmCollection)] - : [typeof(ArmCollection), new CSharpType(typeof(IEnumerable<>), _resource.Type), new CSharpType(typeof(IAsyncEnumerable<>), _resource.Type)]; + ? [] + : [new CSharpType(typeof(IEnumerable<>), _resource.Type), new CSharpType(typeof(IAsyncEnumerable<>), _resource.Type)]; protected override PropertyProvider[] BuildProperties() => []; diff --git a/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MockableMgmtTypeSpecResourceGroupResource.cs b/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MockableMgmtTypeSpecResourceGroupResource.cs new file mode 100644 index 000000000000..7e6e9301099e --- /dev/null +++ b/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MockableMgmtTypeSpecResourceGroupResource.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using Azure.ResourceManager; + +namespace MgmtTypeSpec +{ + /// + public partial class MockableMgmtTypeSpecResourceGroupResource : ArmResource + { + } +} From 8a5333a6e37ec743fbe00a91330298acb0580e58 Mon Sep 17 00:00:00 2001 From: Arcturus Zhang Date: Wed, 28 May 2025 17:08:00 +0800 Subject: [PATCH 2/5] a bunch of updates --- .../src/Azure.Generator.Management.csproj | 6 +- .../src/ManagementOutputLibrary.cs | 8 +- .../src/ManagementTypeFactory.cs | 13 +- .../NewManagementProjectScaffolding.cs | 14 ++ .../src/Providers/ExtensionProvider.cs | 73 ++++++++++ .../src/Providers/MockableResourceProvider.cs | 133 +++++++++++++++++- .../src/Providers/ResourceClientProvider.cs | 4 +- .../Extensions/MgmtTypeSpecExtensions.cs | 21 +++ ...ckableMgmtTypeSpecResourceGroupResource.cs | 46 ++++++ 9 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Primitives/NewManagementProjectScaffolding.cs create mode 100644 eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ExtensionProvider.cs create mode 100644 eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MgmtTypeSpecExtensions.cs diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Azure.Generator.Management.csproj b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Azure.Generator.Management.csproj index fd60acff8447..0b27c3fd6b5f 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Azure.Generator.Management.csproj +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Azure.Generator.Management.csproj @@ -9,6 +9,7 @@ + @@ -91,7 +92,10 @@ Always - + + Always + + Always diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementOutputLibrary.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementOutputLibrary.cs index c5b94ed98f2b..836efd7005c9 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementOutputLibrary.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementOutputLibrary.cs @@ -40,7 +40,7 @@ public class ManagementOutputLibrary : AzureOutputLibrary }; // TODO -- build extensions and their corresponding mockable resources - private IReadOnlyList BuildResourceExtensions(IEnumerable resources) + private IReadOnlyList BuildExtensions(IEnumerable resources) { // walk through all resources to figure out their scopes var scopeCandidates = new Dictionary> @@ -64,8 +64,10 @@ private IReadOnlyList BuildResourceExtensions(IEnumera mockableResources.Add(mockableExtension); } } + var extensionProvider = new ExtensionProvider(mockableResources); + ManagementClientGenerator.Instance.AddTypeToKeep(extensionProvider.Name); - return mockableResources; + return [.. mockableResources, extensionProvider]; } private static void BuildResourceCore(List resources, List collections, InputClient client) @@ -89,7 +91,7 @@ private static void BuildResourceCore(List resources, Li protected override TypeProvider[] BuildTypeProviders() { var (resources, collections) = BuildResources(); - var extensions = BuildResourceExtensions(resources); + var extensions = BuildExtensions(resources); return [ .. base.BuildTypeProviders().Where(t => t is not InheritableSystemObjectModelProvider), ArmOperation, diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementTypeFactory.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementTypeFactory.cs index 87cd29279d6c..7431ab00c78d 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementTypeFactory.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/ManagementTypeFactory.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using Azure.Generator.Management.InputTransformation; -using Azure.Generator.Management.Providers.Abstraction; using Azure.Generator.Management.Primitives; +using Azure.Generator.Management.Providers; +using Azure.Generator.Management.Providers.Abstraction; +using Azure.Generator.Primitives; using Microsoft.TypeSpec.Generator; using Microsoft.TypeSpec.Generator.ClientModel.Providers; using Microsoft.TypeSpec.Generator.Expressions; @@ -12,12 +14,11 @@ using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; using Microsoft.TypeSpec.Generator.Statements; -using System.ClientModel.Primitives; using System; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Text.Json; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; -using Azure.Generator.Management.Providers; namespace Azure.Generator.Management { @@ -104,5 +105,11 @@ public override ValueExpression DeserializeJsonValue(Type valueType, ScopedApi + public override NewProjectScaffolding CreateNewProjectScaffolding() + { + return new NewManagementProjectScaffolding(); + } } } diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Primitives/NewManagementProjectScaffolding.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Primitives/NewManagementProjectScaffolding.cs new file mode 100644 index 000000000000..d163c8f64f31 --- /dev/null +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Primitives/NewManagementProjectScaffolding.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Generator.Primitives; + +namespace Azure.Generator.Management.Primitives +{ + internal class NewManagementProjectScaffolding : NewAzureProjectScaffolding + { + // TODO -- call the hook to add new files that mgmt projects need + // current list: + // - ForwardsClientCallsAttribute.cs + } +} diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ExtensionProvider.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ExtensionProvider.cs new file mode 100644 index 000000000000..aa9dd357f529 --- /dev/null +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ExtensionProvider.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Input.Extensions; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Snippets; +using Microsoft.TypeSpec.Generator.Statements; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using static Microsoft.TypeSpec.Generator.Snippets.Snippet; + +namespace Azure.Generator.Management.Providers +{ + internal class ExtensionProvider : TypeProvider + { + private const string GetCachedClient = "GetCachedClient"; + private const string IdProperty = "Id"; + + private readonly IReadOnlyList _mockableResources; + public ExtensionProvider(IReadOnlyList mockableResources) + { + _mockableResources = mockableResources; + } + + protected override TypeSignatureModifiers BuildDeclarationModifiers() => TypeSignatureModifiers.Public | TypeSignatureModifiers.Static; + + protected override string BuildName() => $"{ManagementClientGenerator.Instance.TypeFactory.ResourceProviderName}Extensions"; + + protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Extensions", $"{Name}.cs"); + + protected override MethodProvider[] BuildMethods() + { + var methods = new List(_mockableResources.Count + _mockableResources.Sum(m => m.Methods.Count)); + + // we have a few methods to get the cached clients for those mockable resources + foreach (var mockableResource in _mockableResources) + { + methods.Add(BuildGetCachedClientMethod(mockableResource)); + } + + // then all the methods are just forwarding to the mockable resource methods + foreach (var mockableResource in _mockableResources) + { + //methods.AddRange(mockableResource.Methods); + } + + return [.. methods]; + } + + private MethodProvider BuildGetCachedClientMethod(MockableResourceProvider mockableResource) + { + var coreType = mockableResource.ArmCoreType; + var parameter = new ParameterProvider(coreType.Name.ToVariableName(), $"", coreType); + var methodSignature = new MethodSignature( + $"Get{mockableResource.Name}", + null, + MethodSignatureModifiers.Public | MethodSignatureModifiers.Static, + mockableResource.Type, + null, + [parameter]); + var clientVar = new CodeWriterDeclaration("client"); + var lambda = new FuncExpression([clientVar], New.Instance(mockableResource.Type, new ValueExpression[] { new VariableExpression(mockableResource.Type, clientVar), parameter.Property(IdProperty) })); + var statements = new List + { + Return(parameter.Invoke(GetCachedClient, lambda)) + }; + return new MethodProvider(methodSignature, statements, this); + } + } +} diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/MockableResourceProvider.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/MockableResourceProvider.cs index 13c42080b48e..4feff116c13e 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/MockableResourceProvider.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/MockableResourceProvider.cs @@ -1,30 +1,157 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.Generator.Management.Utilities; using Azure.ResourceManager; +using Humanizer; +using Microsoft.TypeSpec.Generator.ClientModel.Providers; +using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; +using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using static Microsoft.TypeSpec.Generator.Snippets.Snippet; namespace Azure.Generator.Management.Providers { internal class MockableResourceProvider : TypeProvider { - private readonly CSharpType _armCoreType; private readonly IReadOnlyList _resources; + private const string GetCachedClient = "GetCachedClient"; + private const string IdProperty = "Id"; + // TODO -- in the future we need to update this to include the operations this mockable resource should include. public MockableResourceProvider(CSharpType armCoreType, IReadOnlyList resources) { - _armCoreType = armCoreType; + ArmCoreType = armCoreType; _resources = resources; } + internal CSharpType ArmCoreType { get; } - protected override string BuildName() => $"Mockable{ManagementClientGenerator.Instance.TypeFactory.ResourceProviderName}{_armCoreType.Name}"; + protected override string BuildName() => $"Mockable{ManagementClientGenerator.Instance.TypeFactory.ResourceProviderName}{ArmCoreType.Name}"; protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Extensions", $"{Name}.cs"); protected override CSharpType? GetBaseType() => typeof(ArmResource); + + protected override ConstructorProvider[] BuildConstructors() + => [ConstructorProviderHelper.BuildMockingConstructor(this), BuildResourceIdentifierConstructor()]; + + private ConstructorProvider BuildResourceIdentifierConstructor() + { + var idParameter = new ParameterProvider("id", $"The identifier of the resource that is the target of operations.", typeof(ResourceIdentifier)); + var parameters = new List + { + new("client", $"The client parameters to use in these operations.", typeof(ArmClient)), + idParameter + }; + + var initializer = new ConstructorInitializer(true, parameters); + var signature = new ConstructorSignature( + Type, + $"Initializes a new instance of {Type:C} class.", + MethodSignatureModifiers.Internal, + parameters, + null, + initializer); + + return new ConstructorProvider(signature, MethodBodyStatement.Empty, this); + } + + protected override MethodProvider[] BuildMethods() + { + var methods = new List(_resources.Count * 3); + foreach (var resource in _resources) + { + methods.AddRange(BuildMethodsForResource(resource)); + } + return [.. methods]; + } + + private IEnumerable BuildMethodsForResource(ResourceClientProvider resource) + { + if (resource.IsSingleton) + { + yield break; + } + else + { + var collection = resource.ResourceCollection!; + // the first method is returning the collection + var pluralOfResourceName = resource.SpecName.Pluralize(); + var collectionMethodSignature = new MethodSignature( + $"Get{pluralOfResourceName}", + $"Gets a collection of {pluralOfResourceName} in the {ArmCoreType:C}", + MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, + collection.Type, + $"An object representing collection of {pluralOfResourceName} and their operations over a {resource.Name}.", + [] + ); + var clientVar = new CodeWriterDeclaration("client"); + var lambda = new FuncExpression([clientVar], New.Instance(collection.Type, new ValueExpression[] { new VariableExpression(typeof(ArmClient), clientVar), This.Property(IdProperty) })); + var bodyStatement = new MethodBodyStatement[] + { + Return(This.Invoke(GetCachedClient, lambda)) + }; + yield return new MethodProvider( + collectionMethodSignature, + bodyStatement, + this); + // the second and third methods are the sync and async version of the Get method on the collection + // find the method + var getMethod = collection.Methods.FirstOrDefault(m => m.Signature.Name == "Get"); + var getAsyncMethod = collection.Methods.FirstOrDefault(m => m.Signature.Name == "GetAsync"); + if (getMethod is not null) + { + // we should be sure that this would never be null, but this null check here is just ensuring that we never crash + // TODO -- figure out how to remove the parameter validation + var getResourceMethodSignature = new MethodSignature( + getMethod.Signature.Name, + getMethod.Signature.Description, + getMethod.Signature.Modifiers, + getMethod.Signature.ReturnType, + getMethod.Signature.ReturnDescription, + getMethod.Signature.Parameters); + // TODO -- we need to add the ForwardsClientCallsAttribute attribute when the hook to add more shared source is available + // Attributes: [new AttributeStatement(typeof(ForwardsClientCallsAttribute))]); + + yield return new MethodProvider( + getResourceMethodSignature, + new MethodBodyStatement[] + { + Return(This.Invoke(collectionMethodSignature).Invoke(getMethod.Signature)) + }, + this); + } + + if (getAsyncMethod is not null) + { + // we should be sure that this would never be null, but this null check here is just ensuring that we never crash + // TODO -- figure out how to remove the parameter validation + var getResourceAsyncMethodSignature = new MethodSignature( + getAsyncMethod.Signature.Name, + getAsyncMethod.Signature.Description, + getAsyncMethod.Signature.Modifiers, + getAsyncMethod.Signature.ReturnType, + getAsyncMethod.Signature.ReturnDescription, + getAsyncMethod.Signature.Parameters); + // TODO -- we need to add the ForwardsClientCallsAttribute attribute when the hook to add more shared source is available + // Attributes: [new AttributeStatement(typeof(ForwardsClientCallsAttribute))]); + yield return new MethodProvider( + getResourceAsyncMethodSignature, + new MethodBodyStatement[] + { + Return(This.Invoke(collectionMethodSignature).Invoke(getAsyncMethod.Signature)) + }, + this); + } + } + } } } diff --git a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceClientProvider.cs b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceClientProvider.cs index 1918ab06aa3b..a8c0b4b533a0 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceClientProvider.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/Azure.Generator.Management/src/Providers/ResourceClientProvider.cs @@ -10,6 +10,7 @@ using Microsoft.TypeSpec.Generator.ClientModel.Providers; using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Snippets; @@ -58,7 +59,8 @@ private protected ResourceClientProvider(InputClient inputClient, ResourceMetada var resourceType = resourceMetadata.ResourceType; _resourceTypeField = new FieldProvider(FieldModifiers.Public | FieldModifiers.Static | FieldModifiers.ReadOnly, typeof(ResourceType), "ResourceType", this, description: $"Gets the resource type for the operations.", initializationValue: Literal(resourceType)); var resourceModel = resourceMetadata.ResourceModel; - SpecName = resourceModel.Name; // TODO -- here we need to call the ToCleanName + // TODO -- the name of a resource is not always the name of its model. Maybe the resource metadata should have a property for the name of the resource? + SpecName = resourceModel.Name.ToIdentifierName(); // We should be able to assume that all operations in the resource client are for the same resource var requestPath = new RequestPath(inputClient.Methods.First().Operation.Path); diff --git a/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MgmtTypeSpecExtensions.cs b/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MgmtTypeSpecExtensions.cs new file mode 100644 index 000000000000..fa33f3f99499 --- /dev/null +++ b/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MgmtTypeSpecExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// + +#nullable disable + +using Azure.ResourceManager.Resources; + +namespace MgmtTypeSpec +{ + /// + public static partial class MgmtTypeSpecExtensions + { + /// + public static MockableMgmtTypeSpecResourceGroupResource GetMockableMgmtTypeSpecResourceGroupResource(ResourceGroupResource resourceGroupResource) + { + return resourceGroupResource.GetCachedClient(client => new MockableMgmtTypeSpecResourceGroupResource(client, resourceGroupResource.Id)); + } + } +} diff --git a/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MockableMgmtTypeSpecResourceGroupResource.cs b/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MockableMgmtTypeSpecResourceGroupResource.cs index 7e6e9301099e..a3229bb2383f 100644 --- a/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MockableMgmtTypeSpecResourceGroupResource.cs +++ b/eng/packages/http-client-csharp-mgmt/generator/TestProjects/Local/Mgmt-TypeSpec/src/Generated/Extensions/MockableMgmtTypeSpecResourceGroupResource.cs @@ -5,12 +5,58 @@ #nullable disable +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure; +using Azure.Core; using Azure.ResourceManager; +using Azure.ResourceManager.Resources; namespace MgmtTypeSpec { /// public partial class MockableMgmtTypeSpecResourceGroupResource : ArmResource { + /// Initializes a new instance of MockableMgmtTypeSpecResourceGroupResource for mocking. + protected MockableMgmtTypeSpecResourceGroupResource() + { + } + + /// Initializes a new instance of class. + /// The client parameters to use in these operations. + /// The identifier of the resource that is the target of operations. + internal MockableMgmtTypeSpecResourceGroupResource(ArmClient client, ResourceIdentifier id) : base(client, id) + { + } + + /// Gets a collection of Foos in the . + /// An object representing collection of Foos and their operations over a FooResource. + public virtual FooCollection GetFoos() + { + return GetCachedClient(client => new FooCollection(client, Id)); + } + + /// Get a Foo. + /// The name of the Foo. + /// The cancellation token that can be used to cancel the operation. + /// is null. + public virtual Response Get(string fooName, CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(fooName, nameof(fooName)); + + return GetFoos().Get(fooName, cancellationToken); + } + + /// Get a Foo. + /// The name of the Foo. + /// The cancellation token that can be used to cancel the operation. + /// is null. + public virtual async Task> GetAsync(string fooName, CancellationToken cancellationToken = default) + { + Argument.AssertNotNull(fooName, nameof(fooName)); + + return await GetFoos().GetAsync(fooName, cancellationToken).ConfigureAwait(false); + } } } From 99bf88f83af5d68864434c0493095557fa94ed8a Mon Sep 17 00:00:00 2001 From: Arcturus Zhang Date: Wed, 28 May 2025 17:08:34 +0800 Subject: [PATCH 3/5] dependency update --- eng/Packages.Data.props | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index 60f46b2492c7..0acc25fb20b1 100644 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -209,6 +209,7 @@ +