ResourceVisitor.cs•16.7 kB
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Azure.Bicep.Types;
using Azure.Bicep.Types.Concrete;
using Azure.Bicep.Types.Index;
using AzureMcp.BicepSchema.Services.ResourceProperties.Entities;
using AzureMcp.BicepSchema.Services.Support;
// This is mostly from
// https://msazure.visualstudio.com/One/_git/AzureUX-Deployments-Tooling?path=%2FBicepTypesDefinitions%2FResourcePropertiesApp%2FProgram.cs, which
// is based off of the code in https://github.com/Azure/bicep-types/blob/main/src/bicep-types/src/writers/markdown.ts
namespace AzureMcp.BicepSchema.Services.ResourceProperties;
public class ResourceVisitor
{
    private readonly ITypeLoader _azTypeLoader;
    private readonly Lazy<TypeIndex> _typeIndex;
    // Sample key of a resource: "Microsoft.App/containerApps/authConfigs@2024-03-01"
    private readonly Lazy<IReadOnlyDictionary<string, CrossFileTypeReference>> _resources;
    // Sample key of a resource function: "microsoft.app/containerapps"
    // Sample value of a resource function: { "2024-03-01": [ function1, function2 ], "2024-08-02-preview": [ function1, function2] }
    private readonly Lazy<InsensitiveDictionary<IReadOnlyDictionary<string, IReadOnlyList<CrossFileTypeReference>>>> _resourceFunctions;
    // TODO: Consider adding k8s types
    public ResourceVisitor(ITypeLoader azTypeLoader)
    {
        _azTypeLoader = azTypeLoader;
        _typeIndex = new Lazy<TypeIndex>(azTypeLoader.LoadTypeIndex);
        _resources = new Lazy<IReadOnlyDictionary<string, CrossFileTypeReference>>(() => new InsensitiveDictionary<CrossFileTypeReference>(_typeIndex.Value.Resources.ToDictionary()));
        _resourceFunctions = new Lazy<InsensitiveDictionary<IReadOnlyDictionary<string, IReadOnlyList<CrossFileTypeReference>>>>(
            () => new InsensitiveDictionary<IReadOnlyDictionary<string, IReadOnlyList<CrossFileTypeReference>>>(
                _typeIndex.Value.ResourceFunctions.ToDictionary()));
    }
    public InsensitiveDictionary<ProviderResourceTypes> GetAllResourceTypesAndVersionsByProvider()
    {
        var providers = new InsensitiveDictionary<ProviderResourceTypes>();
        foreach (KeyValuePair<string, CrossFileTypeReference> resource in _resources.Value)
        {
            string[] typeAndVersion = resource.Key.Split('@');
            string fqResourceType = typeAndVersion[0];
            string apiVersion = typeAndVersion[1];
            string provider = fqResourceType.Split("/").First();
            string providerKey = provider.ToLowerInvariant();
            string resourceTypeKey = fqResourceType.ToLowerInvariant();
            if (!providers.TryGetValue(providerKey, out ProviderResourceTypes? providerResourceTypes))
            {
                providerResourceTypes = new ProviderResourceTypes(provider);
                providers[providerKey] = providerResourceTypes;
            }
            if (!providerResourceTypes.ResourceTypes.TryGetValue(resourceTypeKey, out UniqueResourceType? uniqueResourceType))
            {
                uniqueResourceType = new UniqueResourceType(fqResourceType);
                providerResourceTypes.ResourceTypes[resourceTypeKey] = uniqueResourceType;
            }
            uniqueResourceType.ApiVersions.Add(apiVersion);
        }
        return providers;
    }
    public UniqueResourceType? FindResourceTypeAndVersions(string resourceTypeName)
    {
        (string providerName, _, _) = ResourceParser.ParseResourceType(resourceTypeName);
        if (GetAllResourceTypesAndVersionsByProvider().TryGetValue(providerName, out ProviderResourceTypes? provider)
            && provider.ResourceTypes.TryGetValue(resourceTypeName.ToLowerInvariant(), out UniqueResourceType? uniqueResourceType))
        {
            return uniqueResourceType;
        }
        else
        {
            return null;
        }
    }
    public string[] GetResourceApiVersions(string resourceTypeName)
    {
        (string providerName, _, _) = ResourceParser.ParseResourceType(resourceTypeName);
        if (!GetAllResourceTypesAndVersionsByProvider().TryGetValue(providerName, out ProviderResourceTypes? provider))
        {
            throw new Exception($"Resource type {resourceTypeName} not found.");
        }
        return [.. provider.ResourceTypes[resourceTypeName.ToLowerInvariant()].ApiVersions];
    }
    public TypesDefinitionResult LoadSingleResource(string resourceTypeName, string apiVersion)
    {
        (string provider, _, _) = ResourceParser.ParseResourceType(resourceTypeName);
        var typesToWrite = new List<TypeBase>();
        string fullResourceTypeName = $"{resourceTypeName}@{apiVersion}";
        if (!_resources.Value.TryGetValue(fullResourceTypeName, out CrossFileTypeReference? resource))
        {
            // ITypeLoader.LoadResourceType() doesn't tell us whether the type name of apiVersion is incorrect, so
            //   figure it out here.
            if (FindResourceTypeAndVersions(resourceTypeName) is UniqueResourceType uniqueResourceType)
            {
                throw new InvalidDataException(
                    $"Resource type {resourceTypeName} does not have an apiVersion \"{apiVersion}\". "
                    + $"Available versions are: {uniqueResourceType.ApiVersions.JoinWithComma()}");
            }
            else
            {
                throw new InvalidDataException($"Resource type {resourceTypeName} not found.");
            }
        }
        var selectedResourceFunctions = _resourceFunctions.Value.Where(r => r.Key.ContainsOrdinalInsensitively(provider)).ToList();
        var result = new TypesDefinitionResult
        {
            ResourceProvider = provider,
            ApiVersion = apiVersion
        };
        ResourceType resourceType = _azTypeLoader.LoadResourceType(resource);
        FindTypesToWrite(typesToWrite, resourceType.Body);
        if (WriteComplexType(resourceType) is ResourceTypeEntity resourceTypeEntity)
        {
            result.ResourceTypeEntities.Add(resourceTypeEntity);
        }
        else
        {
            throw new InvalidDataException($"Resource type {resourceType.Name} failed to be converted to ResourceTypeEntity.");
        }
        foreach (KeyValuePair<string, IReadOnlyDictionary<string, IReadOnlyList<CrossFileTypeReference>>> resourceFunction in selectedResourceFunctions)
        {
            var functions = resourceFunction.Value.Where(r => r.Key.Equals(apiVersion)).SelectMany(r => r.Value).ToList();
            foreach (CrossFileTypeReference? function in functions)
            {
                ResourceFunctionType resourceFunctionType = _azTypeLoader.LoadResourceFunctionType(function);
                if (resourceFunctionType.Input != null)
                {
                    typesToWrite.Add(resourceFunctionType.Input.Type);
                    FindTypesToWrite(typesToWrite, resourceFunctionType.Input);
                }
                typesToWrite.Add(resourceFunctionType.Output.Type);
                FindTypesToWrite(typesToWrite, resourceFunctionType.Output);
                if (WriteComplexType(resourceFunctionType) is ResourceFunctionTypeEntity resourceFunctionTypeEntity)
                {
                    result.ResourceFunctionTypeEntities.Add(resourceFunctionTypeEntity);
                }
                else
                {
                    throw new InvalidDataException($"Resource function type {resourceFunctionType.Name} failed to be converted to ResourceFunctionTypeEntity.");
                }
            }
        }
        // Sort by name first (e.g. listSecrets), then by resource type (e.g. Microsoft.ApiManagement/service/authorizationServers)
        result.ResourceFunctionTypeEntities.Sort((a, b) =>
        {
            int nameComparison = string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
            return nameComparison != 0 ? nameComparison : string.Compare(a.ResourceType, b.ResourceType, StringComparison.OrdinalIgnoreCase);
        });
        foreach (TypeBase type in typesToWrite)
        {
            if (IsComplexType(type))
            {
                result.OtherComplexTypeEntities.Add(WriteComplexType(type));
            }
        }
        // Note(ligar): Dedupe here because OtherComplexTypeEntities can contain duplicates. This is because instances of the same type (TypeBase) can have different hash codes (Refer to ProcessTypeLinks() method).
        result.OtherComplexTypeEntities = [.. result.OtherComplexTypeEntities
            .GroupBy(e => e.Name)
            .Select(g => g.First())];
        result.OtherComplexTypeEntities.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
        return result;
    }
    // This finds all the types the referenced type depends on, and adds them to the typesToWrite list.
    private void FindTypesToWrite(List<TypeBase> typesToWrite, ITypeReference typeReference)
    {
        switch (typeReference.Type)
        {
            case ArrayType arrayType:
                ProcessTypeLinks(typesToWrite, arrayType.ItemType, false);
                break;
            case ObjectType objectType:
                foreach (KeyValuePair<string, ObjectTypeProperty> property in objectType.Properties.OrderByAscendingOrdinalInsensitively(kvp => kvp.Key))
                {
                    ProcessTypeLinks(typesToWrite, property.Value.Type, false);
                }
                if (objectType.AdditionalProperties != null)
                {
                    ProcessTypeLinks(typesToWrite, objectType.AdditionalProperties, false);
                }
                break;
            case DiscriminatedObjectType discriminatedObjectType:
                foreach (KeyValuePair<string, ITypeReference> property in discriminatedObjectType.Elements.OrderByAscendingOrdinalInsensitively(kvp => kvp.Key))
                {
                    // Don't display discriminated object elements as individual types
                    ProcessTypeLinks(typesToWrite, property.Value, true);
                }
                break;
            default:
                // In this method, we don't care about simple types such as IntegerType
                break;
        }
    }
    private void ProcessTypeLinks(List<TypeBase> typesToWrite, ITypeReference typeReference, bool skipParent)
    {
        if (!typesToWrite.Contains(typeReference.Type))
        {
            if (!skipParent)
            {
                typesToWrite.Add(typeReference.Type);
            }
            FindTypesToWrite(typesToWrite, typeReference);
        }
    }
    private ComplexType WriteComplexType(TypeBase typeBase)
    {
        switch (typeBase)
        {
            case ResourceType resourceType:
                var rtEntity = new ResourceTypeEntity
                {
                    Name = resourceType.Name,
                    BodyType = WriteComplexType(resourceType.Body.Type),
                    WritableScopes = resourceType.WritableScopes.ToString(),
                    ReadableScopes = resourceType.ReadableScopes.ToString(),
                };
                return rtEntity;
            case ResourceFunctionType resourceFunctionType:
                var rftEntity = new ResourceFunctionTypeEntity
                {
                    Name = resourceFunctionType.Name,
                    ResourceType = resourceFunctionType.ResourceType,
                    ApiVersion = resourceFunctionType.ApiVersion,
                    InputType = resourceFunctionType.Input != null ? GetTypeName(resourceFunctionType.Input.Type) : null,
                    OutputType = GetTypeName(resourceFunctionType.Output.Type)
                };
                return rftEntity;
            case ObjectType objectType:
                var otEntity = new ObjectTypeEntity
                {
                    Name = objectType.Name,
                    Sensitive = objectType.Sensitive,
                    AdditionalPropertiesType = objectType.AdditionalProperties != null ? GetTypeName(objectType.AdditionalProperties.Type) : null
                };
                foreach (KeyValuePair<string, ObjectTypeProperty> property in objectType.Properties.OrderByAscendingOrdinalInsensitively(kvp => kvp.Key))
                {
                    otEntity.Properties.Add(WriteTypeProperty(property.Key, property.Value));
                }
                return otEntity;
            case DiscriminatedObjectType discriminatedObjectType:
                var dotEntity = new DiscriminatedObjectTypeEntity
                {
                    Name = discriminatedObjectType.Name,
                    Discriminator = discriminatedObjectType.Discriminator
                };
                foreach (KeyValuePair<string, ObjectTypeProperty> baseProperty in discriminatedObjectType.BaseProperties.OrderByAscendingOrdinalInsensitively(kvp => kvp.Key))
                {
                    dotEntity.BaseProperties.Add(WriteTypeProperty(baseProperty.Key, baseProperty.Value));
                }
                foreach (KeyValuePair<string, ITypeReference> element in discriminatedObjectType.Elements.OrderByAscendingOrdinalInsensitively(kvp => kvp.Key))
                {
                    dotEntity.Elements.Add(WriteComplexType(element.Value.Type));
                }
                return dotEntity;
            default:
                throw new InvalidDataException("Unexpected type");
        }
    }
    private PropertyInfo WriteTypeProperty(string propertyName, ObjectTypeProperty property)
    {
        return new PropertyInfo(
            propertyName,
            GetTypeName(property.Type.Type),
            property.Description,
            GetFlags(property.Flags),
            GetModifiers(property.Type.Type));
    }
    private string GetTypeName(TypeBase typeBase)
    {
        return typeBase switch
        {
            ResourceType resourceType => resourceType.Name,
            ResourceFunctionType resourceFunctionType => $"{resourceFunctionType.Name} ({resourceFunctionType.ResourceType}@{resourceFunctionType.ApiVersion})",
            ObjectType objectType => objectType.Name,
            DiscriminatedObjectType discriminatedObjectType => discriminatedObjectType.Name,
            ArrayType arrayType => $"{GetTypeName(arrayType.ItemType.Type)}[]",
            UnionType unionType => string.Join(" | ", unionType.Elements.Select(e => GetTypeName(e.Type))),
            AnyType anyType => "any",
            NullType nullType => "null",
            BooleanType booleanType => "bool",
            IntegerType integerType => "int",
            StringType stringType => "string",
            StringLiteralType stringLiteralType => stringLiteralType.Value,
            BuiltInType builtInType => builtInType.Kind.ToString().ToLower(),
            _ => throw new InvalidDataException("Unrecognized type"),
        };
    }
    private string? GetFlags(ObjectTypePropertyFlags flags)
    {
        return flags == ObjectTypePropertyFlags.None ? null : flags.ToString();
    }
    private string? GetModifiers(TypeBase typeBase)
    {
        return typeBase switch
        {
            IntegerType integerType => GetIntegerModifiers(integerType),
            StringType stringType => GetStringModifiers(stringType),
            _ => null
        };
    }
    private string GetIntegerModifiers(IntegerType integerType)
    {
        return FormatModifiers(
            integerType.MinValue != null ? $"minValue: {integerType.MinValue}" : null,
            integerType.MaxValue != null ? $"maxValue: {integerType.MaxValue}" : null);
    }
    private string GetStringModifiers(StringType stringType)
    {
        return FormatModifiers(
            stringType.Sensitive == true ? "sensitive" : null,
            stringType.MinLength != null ? $"minLength: {stringType.MinLength}" : null,
            stringType.MaxLength != null ? $"maxLength: {stringType.MaxLength}" : null,
            stringType.Pattern != null ? $"pattern: {stringType.Pattern}" : null);
    }
    private string FormatModifiers(params string?[] modifiers)
    {
        string modifiersString = string.Join(", ", modifiers.Where(m => !string.IsNullOrEmpty(m)));
        return string.IsNullOrEmpty(modifiersString) ? string.Empty : modifiersString;
    }
    private bool IsComplexType(TypeBase typeBase)
    {
        return typeBase switch
        {
            ResourceType => true,
            ResourceFunctionType => true,
            ObjectType => true,
            DiscriminatedObjectType => true,
            _ => false,
        };
    }
}