File: Interaction\PromptBinding.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.CommandLine;
 
namespace Aspire.Cli.Interaction;
 
/// <summary>
/// Binds a CLI option to an interactive prompt.
/// When a prompt method receives this, it first checks the parse result for an explicitly provided
/// option value before prompting interactively. In non-interactive mode, it uses the default value
/// or displays an actionable error naming the required option.
/// </summary>
internal sealed class PromptBinding<T>
{
    private readonly Func<ParseResult, (bool WasProvided, T? Value)> _resolver;
    private readonly ParseResult? _parseResult;
 
    internal PromptBinding(
        ParseResult? parseResult,
        string symbolDisplayName,
        Func<ParseResult, (bool WasProvided, T? Value)> resolver,
        T? defaultValue,
        bool hasExplicitDefault)
        : this(parseResult, symbolDisplayName, resolver, defaultValue, hasExplicitDefault, hasExplicitDefault ? defaultValue : default)
    {
    }
 
    internal PromptBinding(
        ParseResult? parseResult,
        string symbolDisplayName,
        Func<ParseResult, (bool WasProvided, T? Value)> resolver,
        T? defaultValue,
        bool hasExplicitDefault,
        T? nonInteractiveDefaultValue)
    {
        _parseResult = parseResult;
        SymbolDisplayName = symbolDisplayName;
        _resolver = resolver;
        DefaultValue = defaultValue;
        HasExplicitDefault = hasExplicitDefault;
        NonInteractiveDefaultValue = nonInteractiveDefaultValue;
    }
 
    /// <summary>
    /// Gets the display name of the CLI option, formatted for error messages
    /// (e.g. "'--publisher'").
    /// </summary>
    public string SymbolDisplayName { get; }
 
    /// <summary>
    /// Gets the default value to use for interactive prompts when the symbol was not provided.
    /// </summary>
    public T? DefaultValue { get; }
 
    /// <summary>
    /// Gets the default value to use when non-interactive and the symbol was not provided.
    /// </summary>
    public T? NonInteractiveDefaultValue { get; }
 
    /// <summary>
    /// Gets whether a default value was explicitly specified when this binding was created.
    /// When <c>false</c>, prompt methods should throw in non-interactive mode instead of
    /// silently using <c>default(T)</c>.
    /// </summary>
    public bool HasExplicitDefault { get; }
 
    /// <summary>
    /// Resolves the value from the parse result. Returns whether the symbol was explicitly
    /// provided by the user and the resolved value.
    /// </summary>
    public (bool WasProvided, T? Value) Resolve()
    {
        if (_parseResult is null)
        {
            return (false, default);
        }
 
        return _resolver(_parseResult);
    }
 
    /// <summary>
    /// Creates a new <see cref="PromptBinding{T}"/> with the same resolver but a different default value.
    /// </summary>
    public PromptBinding<T> WithDefault(T? newDefault) =>
        new(_parseResult, SymbolDisplayName, _resolver, newDefault, hasExplicitDefault: true, nonInteractiveDefaultValue: newDefault);
}
 
/// <summary>
/// Factory methods for creating <see cref="PromptBinding{T}"/> instances.
/// </summary>
internal static class PromptBinding
{
    public static (bool WasProvided, T? OptionValue, T? DefaultValue) Resolve<T>(PromptBinding<T>? binding)
    {
        if (binding == null)
        {
            return default;
        }
 
        var (wasProvided, optionValue) = binding.Resolve();
        return (wasProvided, optionValue, binding.DefaultValue);
    }
 
    public static PromptBinding<T> Create<T>(ParseResult parseResult, Option<T> option) =>
        new(parseResult, FormatOptionName(option), BuildOptionResolver(option), default, hasExplicitDefault: false);
 
    public static PromptBinding<T> Create<T>(ParseResult parseResult, Option<T> option, T defaultValue) =>
        new(parseResult, FormatOptionName(option), BuildOptionResolver(option), defaultValue, hasExplicitDefault: true);
 
    /// <summary>
    /// Creates a <see cref="PromptBinding{T}"/> with only a default value and no CLI symbol binding.
    /// Use when there is no corresponding CLI option or argument for the prompt.
    /// </summary>
    public static PromptBinding<T> CreateDefault<T>(T defaultValue) =>
        new(null, string.Empty, static _ => (false, default), defaultValue, hasExplicitDefault: true);
 
    /// <summary>
    /// Creates a <see cref="PromptBinding{T}"/> for a <c>bool?</c> option that resolves to the inverse of its value.
    /// This is useful for suppress/disable options that should negate a confirmation result.
    /// When not provided in non-interactive mode, defaults to <paramref name="defaultValue"/>.
    /// </summary>
    public static PromptBinding<bool> CreateInvertedBoolConfirm(ParseResult parseResult, Option<bool?> option, bool defaultValue) =>
        new(parseResult, FormatOptionName(option), BuildResolver<bool?, bool>(option, value => value != true), defaultValue, hasExplicitDefault: true);
 
    /// <summary>
    /// Creates a <see cref="PromptBinding{T}"/> for a <c>bool?</c> option that maps to a confirmation prompt.
    /// When the option is explicitly provided, the binding resolves to <c>true</c> only when the option value is <c>true</c>.
    /// When the option is not explicitly provided, <paramref name="defaultValue"/> is used as the confirmation default,
    /// including as the interactive prompt default when the user accepts the prompt by pressing Enter.
    /// </summary>
    /// <param name="parseResult">The parse result used to determine whether <paramref name="option"/> was explicitly provided.</param>
    /// <param name="option">The nullable Boolean option to bind to the confirmation prompt.</param>
    /// <param name="defaultValue">The default confirmation value to use when <paramref name="option"/> was not explicitly provided.</param>
    /// <returns>
    /// A <see cref="PromptBinding{T}"/> that resolves the explicitly provided <c>bool?</c> option to a <see cref="bool"/>,
    /// where <c>true</c> maps to <c>true</c> and any other value maps to <c>false</c>, and otherwise exposes
    /// <paramref name="defaultValue"/> as the prompt default.
    /// </returns>
    public static PromptBinding<bool> CreateBoolConfirm(ParseResult parseResult, Option<bool?> option, bool defaultValue) =>
        CreateBoolConfirm(parseResult, option, interactiveDefault: defaultValue, nonInteractiveDefault: defaultValue);
 
    /// <summary>
    /// Creates a <see cref="PromptBinding{T}"/> for a <c>bool?</c> option that maps to a confirmation prompt.
    /// When the option is explicitly provided, the binding resolves to <c>true</c> only when the option value is <c>true</c>.
    /// When the option is not explicitly provided, <paramref name="interactiveDefault"/> is used as the confirmation prompt default,
    /// and <paramref name="nonInteractiveDefault"/> is used when interactive input is not available.
    /// </summary>
    /// <param name="parseResult">The parse result used to determine whether <paramref name="option"/> was explicitly provided.</param>
    /// <param name="option">The nullable Boolean option to bind to the confirmation prompt.</param>
    /// <param name="interactiveDefault">The default confirmation value to use for the interactive prompt.</param>
    /// <param name="nonInteractiveDefault">The default confirmation value to use when interactive input is not available.</param>
    /// <returns>
    /// A <see cref="PromptBinding{T}"/> that resolves the explicitly provided <c>bool?</c> option to a <see cref="bool"/>,
    /// where <c>true</c> maps to <c>true</c> and any other value maps to <c>false</c>, and otherwise exposes
    /// <paramref name="interactiveDefault"/> as the prompt default.
    /// </returns>
    public static PromptBinding<bool> CreateBoolConfirm(ParseResult parseResult, Option<bool?> option, bool interactiveDefault, bool nonInteractiveDefault) =>
        new(parseResult, FormatOptionName(option), BuildResolver<bool?, bool>(option, value => value == true), interactiveDefault, hasExplicitDefault: true, nonInteractiveDefaultValue: nonInteractiveDefault);
 
    private static string FormatOptionName<T>(Option<T> option) => $"'{option.Name}'";
 
    private static Func<ParseResult, (bool, TResult?)> BuildResolver<TOption, TResult>(
        Option<TOption> option, Func<TOption?, TResult?> transform) =>
        parseResult =>
        {
            var result = parseResult.GetResult(option);
            if (result is not null && !result.Implicit)
            {
                return (true, transform(parseResult.GetValue(option)));
            }
 
            return (false, default);
        };
 
    private static Func<ParseResult, (bool, T?)> BuildOptionResolver<T>(Option<T> option) =>
        BuildResolver<T, T>(option, static value => value);
}