Skip to content

(csharp part) resource scope support #50158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -28,20 +32,55 @@ public class ManagementOutputLibrary : AzureOutputLibrary
return (resources, collections);
}

private static void BuildResourceCore(List<ResourceClientProvider> resources, List<ResourceCollectionClientProvider> collections, Microsoft.TypeSpec.Generator.Input.InputClient client)
private static readonly IReadOnlyDictionary<ResourceScope, Type> _scopeToTypes = new Dictionary<ResourceScope, Type>
{
[ResourceScope.ResourceGroup] = typeof(ResourceGroupResource),
[ResourceScope.Subscription] = typeof(SubscriptionResource),
[ResourceScope.Tenant] = typeof(TenantResource),
};

// TODO -- build extensions and their corresponding mockable resources
private IReadOnlyList<MockableResourceProvider> BuildResourceExtensions(IEnumerable<ResourceClientProvider> resources)
{
// walk through all resources to figure out their scopes
var scopeCandidates = new Dictionary<ResourceScope, List<ResourceClientProvider>>
{
[ResourceScope.ResourceGroup] = [],
[ResourceScope.Subscription] = [],
[ResourceScope.Tenant] = [],
};
foreach (var resource in resources)
{
scopeCandidates[resource.ResourceScope].Add(resource);
}

var mockableResources = new List<MockableResourceProvider>(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<ResourceClientProvider> resources, List<ResourceCollectionClientProvider> 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);
}
}
}
Expand All @@ -50,12 +89,14 @@ private static void BuildResourceCore(List<ResourceClientProvider> 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)];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ namespace Azure.Generator.Management
/// <inheritdoc/>
public class ManagementTypeFactory : AzureTypeFactory
{
private string? _resourceProviderName;

/// <summary>
/// The name for this resource provider.
/// For instance, if the namespace is "Azure.ResourceManager.Compute", the resource provider name will be "Compute".
/// </summary>
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;
}

/// <inheritdoc/>
public override IClientPipelineApi ClientPipelineApi => MgmtHttpPipelineProvider.Instance;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ResourceClientProvider> _resources;

// TODO -- in the future we need to update this to include the operations this mockable resource should include.
public MockableResourceProvider(CSharpType armCoreType, IReadOnlyList<ResourceClientProvider> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,52 @@ namespace Azure.Generator.Management.Providers
/// </summary>
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<InputServiceMethod> _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<string> GetContextualParameters(string contextualRequestPath)
{
var contextualParameters = new List<string>();
Expand Down Expand Up @@ -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()
};

Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -431,10 +448,10 @@ private ValueExpression[] PopulateArguments(IReadOnlyList<ParameterProvider> 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<bool> TryGetApiVersion(out ScopedApi<string> apiVersion)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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() => [];

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.