File: ConfigModel\ParameterSymbol.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;
using System.Text.Json.Nodes;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Utils;

namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel
{
    /// <summary>
    /// Defines the symbol of type "parameter".
    /// </summary>
    public sealed class ParameterSymbol : BaseValueSymbol
    {
        internal const string TypeName = "parameter";

        private IReadOnlyDictionary<string, ParameterChoice>? _choices;
        private string? _description;
        private string? _displayName;

        /// <summary>
        /// Creates an instance of <see cref="ParameterSymbol"/> using
        /// the provided JSON Data.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="jObject">JSON to initialize the symbol with.</param>
        /// <param name="defaultOverride"></param>
        internal ParameterSymbol(string name, JsonObject jObject, string? defaultOverride)
            : base(name, jObject, defaultOverride, true)
        {
            DefaultIfOptionWithoutValue = jObject.ToString(nameof(DefaultIfOptionWithoutValue));
            DisplayName = jObject.ToString(nameof(DisplayName)) ?? string.Empty;
            Description = jObject.ToString(nameof(Description)) ?? string.Empty;

            var choicesAndDescriptions = new Dictionary<string, ParameterChoice>();

            if (DataType == "choice")
            {
                TagName = jObject.ToString(nameof(TagName));

                foreach (JsonObject choiceObject in jObject.Items<JsonObject>(nameof(Choices)))
                {
                    string? choiceName = choiceObject.ToString("choice");

                    if (string.IsNullOrWhiteSpace(choiceName))
                    {
                        throw new TemplateAuthoringException(string.Format(LocalizableStrings.SymbolModel_Error_MandatoryPropertyMissing, name, ParameterSymbol.TypeName, "choice"), name);
                    }

                    var choice = new ParameterChoice(
                        choiceObject.ToString("displayName") ?? string.Empty,
                        choiceObject.ToString("description") ?? string.Empty);

                    choicesAndDescriptions.Add(choiceName!, choice);
                }
            }
            else if (DataType == "bool" && string.IsNullOrEmpty(DefaultIfOptionWithoutValue))
            {
                // bool flags are considered true if they're provided without a value.
                DefaultIfOptionWithoutValue = "true";
            }

            if (DefaultValue == null && !IsRequired)
            {
                DefaultValue = ParameterConverter.GetDefault(DataType);
            }

            Choices = choicesAndDescriptions;
            AllowMultipleValues = jObject.ToBool(nameof(AllowMultipleValues));
            EnableQuotelessLiterals = jObject.ToBool(nameof(EnableQuotelessLiterals));

            this.Precedence = GetPrecedence(IsRequired, jObject);
        }

        /// <summary>
        /// Creates a clone of the given <see cref="ParameterSymbol"/>.
        /// </summary>
        /// <param name="cloneFrom">The symbol to copy the values from.</param>
        /// <param name="formsFallback">The value to be used for <see cref="BaseValueSymbol.Forms"/> in the case
        /// that the <paramref name="cloneFrom"/> does not specify a value for <see cref="BaseValueSymbol.Forms"/>.</param>
        internal ParameterSymbol(ParameterSymbol cloneFrom, SymbolValueFormsModel formsFallback) : base(cloneFrom, formsFallback)
        {
            Description = cloneFrom.Description;
            IsTag = cloneFrom.IsTag;
            TagName = cloneFrom.TagName;
            Choices = cloneFrom.Choices;
            AllowMultipleValues = cloneFrom.AllowMultipleValues;
            EnableQuotelessLiterals = cloneFrom.EnableQuotelessLiterals;
            Precedence = cloneFrom.Precedence;
        }

        /// <summary>
        /// Creates a default instance of <see cref="ParameterSymbol"/>.
        /// </summary>
        internal ParameterSymbol(string name, string? replaces = null) : base(name, replaces)
        {
            Precedence = TemplateParameterPrecedence.Default;
        }

        public override string Type => TypeName;

        /// <summary>
        /// Gets the friendly name of the symbol to be displayed to the user.
        /// </summary>
        public string? DisplayName
        {
            get => _displayName;
            internal init => _displayName = value;
        }

        /// <summary>
        /// Gets the description of the parameter.
        /// </summary>
        public string? Description
        {
            get => _description;
            internal init => _description = value;
        }

        /// <summary>
        /// If this is set, the option can be provided without a value. It will be given this value.
        /// </summary>
        public string? DefaultIfOptionWithoutValue { get; internal init; }

        /// <summary>
        /// If this is set, it's allowed to specify multiple values of that parameter.
        /// </summary>
        public bool AllowMultipleValues { get; internal init; }

        /// <summary>
        ///  If this is set, it's allowed to specify choice literals without quotation within conditions.
        /// </summary>
        public bool EnableQuotelessLiterals { get; internal init; }

        public TemplateParameterPrecedence Precedence { get; internal init; }

        public string? IsEnabledCondition { get; internal init; }

        public string? IsRequiredCondition { get; internal init; }

        public IReadOnlyDictionary<string, ParameterChoice>? Choices
        {
            get => _choices;

            internal init => _choices = value?.CloneIfDifferentComparer(StringComparer.OrdinalIgnoreCase);
        }

        // only relevant for choice datatype
        internal bool IsTag { get; init; }

        // only relevant for choice datatype
        internal string? TagName { get; init; }

        internal static ParameterSymbol FromDeprecatedConfigTag(string name, string value)
        {
            ParameterSymbol symbol = new ParameterSymbol(name)
            {
                DefaultValue = value,
                DataType = "choice",
                Precedence = GetPrecedence(false, true, true, null, null),
                Choices = new Dictionary<string, ParameterChoice>()
                {
                    { value, new ParameterChoice(string.Empty, string.Empty) }
                },
                Forms = SymbolValueFormsModel.Default
            };

            return symbol;
        }

        internal void Localize(IParameterSymbolLocalizationModel locModel)
        {
            _displayName = locModel.DisplayName ?? _displayName;
            _description = locModel.Description ?? _description;
            if (Choices == null)
            {
                return;
            }

            foreach (KeyValuePair<string, ParameterChoice> choice in Choices)
            {
                if (locModel.Choices.TryGetValue(choice.Key, out ParameterChoiceLocalizationModel? locChoiceModel))
                {
                    choice.Value.Localize(locChoiceModel);
                }
            }

        }

        private static TemplateParameterPrecedence GetPrecedence(bool isRequired, JsonObject jObject)
        {
            string? isRequiredCondition = ParseIsRequiredConditionField(jObject);

            // Initialize IsEnabled - as a condition or a constant
            string? isEnabledCondition = null;
            bool isEnabled = true;
            if (jObject != null && jObject.TryGetValue("IsEnabled", out JsonNode? isEnabledToken))
            {
                if (isEnabledToken!.TryParseBool(out bool enabledConst))
                {
                    isEnabled = enabledConst;
                }
                else if (isEnabledToken!.GetValueKind() == JsonValueKind.String)
                {
                    isEnabledCondition = isEnabledToken.ToString();
                }
            }

            return GetPrecedence(isRequired, isEnabled, false, isRequiredCondition, isEnabledCondition);
        }

        private static TemplateParameterPrecedence GetPrecedence(bool isRequired, bool isEnabled, bool isTag, string? isRequiredCondition, string? isEnabledCondition)
        {
            // If enable condition is set - parameter is conditionally disabled (regardless if require condition is set or not)
            // Conditionally required is if and only if the only require condition is set

            if (!isEnabled)
            {
                return new TemplateParameterPrecedence(PrecedenceDefinition.Disabled);
            }

            if (!string.IsNullOrEmpty(isEnabledCondition))
            {
                return new TemplateParameterPrecedence(PrecedenceDefinition.ConditionalyDisabled, isRequiredCondition, isEnabledCondition, isRequired);
            }

            if (isTag)
            {
                return new TemplateParameterPrecedence(PrecedenceDefinition.Implicit);
            }

            if (!string.IsNullOrEmpty(isRequiredCondition))
            {
                return new TemplateParameterPrecedence(PrecedenceDefinition.ConditionalyRequired, isRequiredCondition, null);
            }

            if (isRequired)
            {
                return new TemplateParameterPrecedence(PrecedenceDefinition.Required, null, null, true);
            }

            return TemplateParameterPrecedence.Default;
        }

        private static string? ParseIsRequiredConditionField(JsonNode token)
        {
            if (!token.TryGetValue(nameof(IsRequired), out JsonNode? isRequiredToken))
            {
                return null;
            }

            // Attribute parsable as a bool - so we do not want to present it as a condition
            if (isRequiredToken!.TryParseBool(out _))
            {
                return null;
            }

            if (isRequiredToken!.GetValueKind() != JsonValueKind.String)
            {
                throw new ArgumentException(string.Format(LocalizableStrings.Symbol_Error_IsRequiredNotABoolOrString, isRequiredToken));
            }

            return isRequiredToken.ToString();
        }
    }
}