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

using System.Text.Json.Nodes;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Localization;
using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Validation;

namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel
{
    public sealed class PostActionModel : ConditionedConfigurationElement
    {
        /// <summary>
        /// Default id to be used when post action contains only one manual instruction
        /// and the author has not explicitly specified an id.
        /// </summary>
        internal const string DefaultIdForSingleManualInstruction = "default";

        private string? _description;

        internal PostActionModel()
            : this(new Dictionary<string, string>(), new List<ManualInstructionModel>()) { }

        internal PostActionModel(IReadOnlyDictionary<string, string> args, IReadOnlyList<ManualInstructionModel> manualInstructions)
        {
            Args = args;
            ManualInstructionInfo = manualInstructions;
        }

        /// <summary>
        /// Gets a string that uniquely identifies this post action within a template.
        /// </summary>
        public string? Id { get; internal init; }

        /// <summary>
        /// Gets the description of the post action.
        /// </summary>
        public string? Description
        {
            get => _description;

            internal init => _description = value;
        }

        /// <summary>
        /// Gets the identifier of the action that will be performed.
        /// Note that this is not an identifier for the post action itself.
        /// </summary>
        public Guid ActionId { get; internal init; }

        /// <summary>
        /// Gets a value indicating whether the template instantiation should continue
        /// in case of an error with this post action.
        /// </summary>
        public bool ContinueOnError { get; internal init; }

        /// <summary>
        /// Gets the arguments for this post action.
        /// </summary>
        public IReadOnlyDictionary<string, string> Args { get; internal init; } = new Dictionary<string, string>();

        /// <summary>
        /// Gets the list of arguments to which file renames should be applied.
        /// </summary>
        public IReadOnlyList<string> ApplyFileRenamesToArgs { get; internal init; } = [];

        /// <summary>
        /// Gets a value indicating whether the file renames should be applied to manual instructions.
        /// </summary>
        public bool ApplyFileRenamesToManualInstructions { get; internal init; }

        /// <summary>
        /// Gets the list of instructions that should be manually performed by the user.
        /// "instruction" contains the text that explains the steps to be taken by the user.
        /// An instruction is only considered if the "condition" evaluates to true.
        /// </summary>
        public IReadOnlyList<ManualInstructionModel> ManualInstructionInfo { get; internal init; } = new List<ManualInstructionModel>();

        internal static IReadOnlyList<PostActionModel> LoadListFromJArray(JsonArray? jArray, List<IValidationEntry> validationEntries)
        {
            List<PostActionModel> localizedPostActions = new();
            if (jArray == null)
            {
                return localizedPostActions;
            }

            HashSet<string> postActionIds = new();
            for (int postActionIndex = 0; postActionIndex < jArray.Count; postActionIndex++)
            {
                JsonNode? action = jArray[postActionIndex];
                string? postActionId = action.ToString(nameof(Id));
                string? description = action.ToString(nameof(Description));
                Guid actionId = action!.ToGuid(nameof(ActionId));
                bool continueOnError = action.ToBool(nameof(ContinueOnError));
                string? postActionCondition = action.ToString(nameof(Condition));
                bool applyFileRenamesToManualInstructions = action.ToBool(nameof(ApplyFileRenamesToManualInstructions));
                IReadOnlyList<string> applyFileRenamesToArgs = action.ArrayAsStrings(nameof(ApplyFileRenamesToArgs)) ?? [];

                if (postActionId != null && !postActionIds.Add(postActionId))
                {
                    // There is already a post action with the same id. Localization won't work properly. Let user know.
                    validationEntries.Add(
                        new ValidationEntry(
                            IValidationEntry.SeverityLevel.Warning,
                            "CONFIG0201",
                            string.Format(LocalizableStrings.Authoring_CONFIG0201_PostActionIdIsNotUnique, postActionId, postActionIndex)));
                    postActionId = null;
                }

                if (actionId == default)
                {
                    validationEntries.Add(
                    new ValidationEntry(
                        IValidationEntry.SeverityLevel.Error,
                        "CONFIG0202",
                        string.Format(LocalizableStrings.Authoring_CONFIG0202_PostActionMustHaveActionId, postActionIndex)));
                    continue;
                }

                Dictionary<string, string> args = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                foreach (var argInfo in action.PropertiesOf("Args"))
                {
                    args.Add(argInfo.Key, argInfo.Value?.ToString() ?? string.Empty);
                }

                List<string> verifiedApplyFileRenamesToArgs = new();

                foreach (string arg in applyFileRenamesToArgs)
                {
                    if (!args.ContainsKey(arg))
                    {
                        validationEntries.Add(
                         new ValidationEntry(
                             IValidationEntry.SeverityLevel.Warning,
                             "CONFIG0204",
                             string.Format(LocalizableStrings.Authoring_CONFIG0204_UnknownArgumentForReplace, arg, nameof(ApplyFileRenamesToArgs).ToCamelCase(), nameof(Args).ToCamelCase())));
                    }
                    else
                    {
                        verifiedApplyFileRenamesToArgs.Add(arg);
                    }
                }

                IReadOnlyList<ManualInstructionModel> manualInstructions = LoadManualInstructionsFromJArray(action.Get<JsonArray>("ManualInstructions"), validationEntries);

                PostActionModel model = new(args, manualInstructions)
                {
                    Id = postActionId,
                    Description = description,
                    ActionId = actionId,
                    ContinueOnError = continueOnError,
                    Condition = postActionCondition,
                    ApplyFileRenamesToArgs = verifiedApplyFileRenamesToArgs,
                    ApplyFileRenamesToManualInstructions = applyFileRenamesToManualInstructions,
                };

                localizedPostActions.Add(model);
            }

            return localizedPostActions;
        }

        internal void Localize(PostActionLocalizationModel locModel)
        {
            _description = locModel.Description ?? Description;

            foreach (var manualInstruction in ManualInstructionInfo)
            {
                string localizedInstruction = string.Empty;
                bool exactIdMatch = manualInstruction.Id != null && locModel.Instructions.TryGetValue(manualInstruction.Id, out localizedInstruction);
                bool defaultIdMatch = manualInstruction.Id == null && ManualInstructionInfo.Count == 1 && locModel.Instructions.TryGetValue(DefaultIdForSingleManualInstruction, out localizedInstruction);

                if (exactIdMatch || defaultIdMatch)
                {
                    manualInstruction.Localize(localizedInstruction);
                }
            }
        }

        private static IReadOnlyList<ManualInstructionModel> LoadManualInstructionsFromJArray(JsonArray? jArray, List<IValidationEntry> validationEntries)
        {
            var results = new List<ManualInstructionModel>();
            if (jArray == null)
            {
                return results;
            }

            HashSet<string> manualInstructionIds = new HashSet<string>();
            for (int i = 0; i < jArray.Count; i++)
            {
                JsonNode? jToken = jArray[i];
                string? id = jToken.ToString("id");
                string text = jToken.ToString("text") ?? string.Empty;
                string? condition = jToken.ToString("condition");

                if (id != null && !manualInstructionIds.Add(id))
                {
                    // There is already an instruction with the same id. We won't be able to localize this. Let user know.
                    validationEntries.Add(
                    new ValidationEntry(
                        IValidationEntry.SeverityLevel.Warning,
                        "CONFIG0203",
                        string.Format(LocalizableStrings.Authoring_CONFIG0203_ManualInstructionIdIsNotUnique, id, i)));
                    id = null;
                }

                results.Add(new ManualInstructionModel(id, text, condition));
            }

            return results;
        }
    }
}