| File: Templating\TemplateParameterPromptState.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; using Aspire.Cli.Resources; using Microsoft.Extensions.Logging; using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Core; using Microsoft.TemplateEngine.Core.Expressions.Cpp2; using Microsoft.TemplateEngine.Utils; namespace Aspire.Cli.Templating; internal sealed class TemplateParameterPromptState { private readonly IEngineEnvironmentSettings _settings; private readonly IGenerator _generator; private readonly ILogger _logger; private readonly Dictionary<string, object> _values = new(StringComparer.Ordinal); private readonly IReadOnlySet<string> _computedParameters; public TemplateParameterPromptState( IEngineEnvironmentSettings settings, ITemplateInfo template, string name, ILogger logger, IReadOnlySet<string> computedParameters) { _settings = settings; _logger = logger; _computedParameters = computedParameters; if (!settings.Components.TryGetComponent(template.GeneratorId, out IGenerator? generator) || generator is null) { throw new InvalidOperationException($"Template generator '{template.GeneratorId}' is not registered."); } _generator = generator; Parameters = OrderParameters(template.ParameterDefinitions.ToArray()); foreach (var parameter in Parameters) { if (parameter.Precedence.PrecedenceDefinition == PrecedenceDefinition.Disabled) { continue; } var value = parameter.IsName ? name : settings.Host.TryGetHostParamDefault(parameter.Name, out var hostDefault) ? hostDefault : parameter.Precedence.CanBeRequired ? null : parameter.DefaultValue; if (value is not null) { // TemplateCreator validates invalid defaults; an interactive answer can still replace them. TrySetValue(parameter, value); } } } public IEnumerable<ITemplateParameter> Parameters { get; } public bool IsEnabled(ITemplateParameter parameter) { if (EvaluateCondition(parameter, parameter.Precedence.IsEnabledCondition, defaultValue: true)) { return true; } // The engine removes disabled values before evaluating dependent parameters' conditions. _values.Remove(parameter.Name); return false; } public bool IsRequired(ITemplateParameter parameter) => parameter.Precedence.IsRequired || EvaluateCondition(parameter, parameter.Precedence.IsRequiredCondition, defaultValue: false); public bool TrySetValue(ITemplateParameter parameter, string value) { var converted = _generator.ConvertParameterValueToType(_settings, parameter, value, out var error); if (error || converted is null) { return false; } _values[parameter.Name] = converted; return true; } private bool EvaluateCondition(ITemplateParameter parameter, string? condition, bool defaultValue) { if (string.IsNullOrEmpty(condition)) { return defaultValue; } if (!_generator.TryEvaluateFromString(_logger, condition, _values, out var result, out var error, null)) { throw new TemplateAuthoringException($"{parameter.Name}: {error}", parameter.Name); } return result; } private IReadOnlyList<ITemplateParameter> OrderParameters(ITemplateParameter[] parameters) { // Parse with every symbol in scope to find references even when its value is not collected yet. // The engine parser respects quoted literals and captures both sides of Boolean expressions. var symbols = new VariableCollection(null, parameters.ToDictionary(parameter => parameter.Name, _ => (object)string.Empty)); var dependencies = new Dictionary<ITemplateParameter, HashSet<string>>(); foreach (var parameter in parameters) { var references = new HashSet<string>(StringComparer.Ordinal); foreach (var condition in new[] { parameter.Precedence.IsEnabledCondition, parameter.Precedence.IsRequiredCondition }) { if (string.IsNullOrEmpty(condition)) { continue; } if (Cpp2StyleEvaluatorDefinition.GetEvaluableExpression(_logger, condition, symbols, out var error, references) is null || !string.IsNullOrEmpty(error)) { throw new TemplateAuthoringException($"{parameter.Name}: {error ?? condition}", parameter.Name); } } if (references.Overlaps(_computedParameters)) { // NuGet policy can depend on answers, so computed values are resolved after // prompting. They cannot also control that earlier prompting phase. throw new TemplateAuthoringException( string.Format(CultureInfo.CurrentCulture, TemplatingStrings.ComputedTemplateParameterCondition, parameter.Name), parameter.Name); } references.Remove(parameter.Name); dependencies[parameter] = references; } var remaining = parameters.ToList(); var ordered = new List<ITemplateParameter>(parameters.Length); while (remaining.Count > 0) { var next = remaining.FindIndex(parameter => !remaining.Any(other => dependencies[parameter].Contains(other.Name))); if (next < 0) { // Cyclic conditions have no dependency-first prompt order. Keep declaration order; // TemplateCreator validates whether the final values form a valid configuration. _logger.LogWarning("Template parameter conditions are cyclic; using declaration order for {Parameters}.", string.Join(", ", remaining.Select(parameter => parameter.Name))); ordered.AddRange(remaining); break; } ordered.Add(remaining[next]); remaining.RemoveAt(next); } return ordered; } }