File: Alias\AliasRegistry.cs
Web Access
Project: src\src\sdk\src\Cli\Microsoft.TemplateEngine.Cli\Microsoft.TemplateEngine.Cli.csproj (Microsoft.TemplateEngine.Cli)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Utils;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace Microsoft.TemplateEngine.Cli.Alias
{
    internal class AliasRegistry
    {
        private readonly IEngineEnvironmentSettings _environmentSettings;
        private readonly string _aliasesFilePath;
        private AliasModel? _aliases;

        internal AliasRegistry(IEngineEnvironmentSettings environmentSettings)
        {
            _environmentSettings = environmentSettings;
            _aliasesFilePath = Path.Combine(_environmentSettings.Paths.HostVersionSettingsDir, "aliases.json");
        }

        internal IReadOnlyDictionary<string, IReadOnlyList<string>> AllAliases
        {
            get
            {
                EnsureLoaded();
                //ensure loaded sets aliases
                return new Dictionary<string, IReadOnlyList<string>>(_aliases!.CommandAliases, StringComparer.OrdinalIgnoreCase);
            }
        }

        internal AliasManipulationResult TryCreateOrRemoveAlias(string aliasName, IReadOnlyList<string> aliasTokens)
        {
            EnsureLoaded();

            if (aliasName == null)
            {
                // the input was malformed. Alias flag without alias name
                return new AliasManipulationResult(AliasManipulationStatus.InvalidInput);
            }
            else if (aliasTokens.Count == 0)
            {
                // the command was just "--alias <alias name>"
                // remove the alias
                //ensure loaded sets aliases
                if (_aliases!.TryRemoveCommandAlias(aliasName, out IReadOnlyList<string>? removedAliasTokens))
                {
                    Save();
                    return new AliasManipulationResult(AliasManipulationStatus.Removed, aliasName, removedAliasTokens);
                }
                else
                {
                    return new AliasManipulationResult(AliasManipulationStatus.RemoveNonExistentFailed, aliasName, null);
                }
            }

            //ensure loaded sets aliases
            Dictionary<string, IReadOnlyList<string>> aliasesWithCandidate = new(_aliases!.CommandAliases);
            aliasesWithCandidate[aliasName] = aliasTokens;
            if (!TryExpandCommandAliases(aliasesWithCandidate, aliasTokens, out IReadOnlyList<string>? expandedInputTokens))
            {
                return new AliasManipulationResult(AliasManipulationStatus.WouldCreateCycle, aliasName, aliasTokens);
            }

            _aliases.AddCommandAlias(aliasName, aliasTokens);
            Save();
            return new AliasManipulationResult(AliasManipulationStatus.Created, aliasName, aliasTokens);
        }

        // Attempts to expand aliases on the input string, using the aliases in _aliases
        internal bool TryExpandCommandAliases(IReadOnlyList<string> inputTokens, out IReadOnlyList<string> expandedInputTokens)
        {
            EnsureLoaded();

            if (inputTokens.Count == 0)
            {
                expandedInputTokens = new List<string>(inputTokens);
                return true;
            }

            //ensure loaded sets aliases
            if (TryExpandCommandAliases(_aliases!.CommandAliases, inputTokens, out expandedInputTokens!))
            {
                return true;
            }

            // TryExpandCommandAliases() returned false because was an expansion error
            expandedInputTokens = new List<string>();
            return false;
        }

        private static bool TryExpandCommandAliases(IReadOnlyDictionary<string, IReadOnlyList<string>> aliases, IReadOnlyList<string> inputTokens, out IReadOnlyList<string>? expandedTokens)
        {
            bool expansionOccurred = false;
            HashSet<string> seenAliases = new();
            expandedTokens = new List<string>(inputTokens);

            do
            {
                string candidateAliasName = expandedTokens[0];

                if (aliases.TryGetValue(candidateAliasName, out IReadOnlyList<string>? aliasExpansion))
                {
                    if (!seenAliases.Add(candidateAliasName))
                    {
                        // a cycle has occurred.... not allowed.
                        expandedTokens = null;
                        return false;
                    }

                    // The expansion is the combination of the aliasExpansion (expands the 0th token of the previously expandedTokens)
                    //  and the rest of the previously expandedTokens
                    expandedTokens = new CombinedList<string>(aliasExpansion, expandedTokens.ToList().GetRange(1, expandedTokens.Count - 1));
                    expansionOccurred = true;
                }
                else
                {
                    expansionOccurred = false;
                }
            }
            while (expansionOccurred);

            return true;
        }

        private void EnsureLoaded()
        {
            if (_aliases != null)
            {
                return;
            }

            if (!_environmentSettings.Host.FileSystem.FileExists(_aliasesFilePath))
            {
                _aliases = new AliasModel();
                return;
            }
            JsonObject parsed = _environmentSettings.Host.FileSystem.ReadObject(_aliasesFilePath);
            IReadOnlyDictionary<string, IReadOnlyList<string>> commandAliases = ToStringListDictionary(parsed, StringComparer.OrdinalIgnoreCase, "CommandAliases");

            _aliases = new AliasModel(commandAliases);
        }

        private void Save()
        {
            if (_aliases is AliasModel { CommandAliases: { Count: > 0 } })
            {
                JsonObject root = new();
                JsonObject commandAliases = new();
                foreach (var kvp in _aliases.CommandAliases)
                {
                    JsonArray arr = new();
                    foreach (string item in kvp.Value)
                    {
                        arr.Add((JsonNode)JsonValue.Create(item)!);
                    }
                    commandAliases[kvp.Key] = arr;
                }
                root["CommandAliases"] = commandAliases;
                _environmentSettings.Host.FileSystem.WriteObject(_aliasesFilePath, root);
            }
            else
            {
                _environmentSettings.Host.FileSystem.FileDelete(_aliasesFilePath);
            }
        }

        // reads a dictionary whose values can either be string literals, or arrays of strings.
        private IReadOnlyDictionary<string, IReadOnlyList<string>> ToStringListDictionary(JsonObject token, StringComparer? comparer = null, string? propertyName = null)
        {
            Dictionary<string, IReadOnlyList<string>> result = new(comparer ?? StringComparer.Ordinal);

            if (propertyName == null)
            {
                return result;
            }

            // Case-insensitive property lookup for compatibility with Newtonsoft.Json behavior
            JsonNode? element = null;
            foreach (var prop in token)
            {
                if (string.Equals(prop.Key, propertyName, StringComparison.OrdinalIgnoreCase))
                {
                    element = prop.Value;
                    break;
                }
            }

            if (element is not JsonObject jObj)
            {
                return result;
            }

            foreach (KeyValuePair<string, JsonNode?> property in jObj)
            {
                if (property.Value == null)
                {
                    continue;
                }
                else if (property.Value.GetValueKind() == JsonValueKind.String)
                {
                    result[property.Key] = new List<string>() { property.Value.GetValue<string>() };
                }
                else if (property.Value is JsonArray arr)
                {
                    List<string> values = new();
                    foreach (JsonNode? item in arr)
                    {
                        if (item != null && item.GetValueKind() == JsonValueKind.String)
                        {
                            values.Add(item.GetValue<string>());
                        }
                    }
                    result[property.Key] = values;
                }
            }

            return result;
        }
    }
}