File: Packaging\IntegrationSourceConfiguration.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.ComponentModel;
using System.Globalization;
using System.Text.Json.Serialization;
using Aspire.Cli.Configuration;
using Aspire.Cli.Resources;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Configuration;
 
namespace Aspire.Cli.Packaging;
 
internal sealed class IntegrationSourceConfiguration
{
    internal const string SectionName = "integrationSources";
    internal const string DefaultName = "default";
 
    [JsonPropertyName("enabled")]
    [Description("Whether to search this source. Defaults to true. Disable default to omit built-in channel and NuGet discovery.")]
    public bool? Enabled { get; init; }
 
    [JsonPropertyName("source")]
    [Description("NuGet service index URL or local package directory. The default entry uses built-in discovery when omitted.")]
    public string? Source { get; init; }
 
    [JsonPropertyName("patterns")]
    [Description("Optional allowed package IDs: exact IDs, prefixes ending in *, or * for all packages. Defaults to * when omitted.")]
    public string[]? Patterns { get; init; }
 
    [JsonIgnore]
    internal string Name { get; init; } = DefaultName;
 
    internal bool AllowsPackage(string packageId)
        => Patterns!.Any(pattern => PackageSourceOverrideMappings.MatchesPackage(pattern, packageId));
 
    internal IEnumerable<string> SearchTerms
        => Patterns!.Contains("*")
            ? [""]
            : Patterns!.Select(pattern => pattern.TrimEnd('*')).Distinct(StringComparer.OrdinalIgnoreCase);
 
    internal static IReadOnlyList<IntegrationSourceConfiguration> Resolve(DirectoryInfo directory, FileInfo globalSettingsFile)
    {
        using var configuration = new ConfigurationManager();
        configuration.AddEnvironmentVariables();
        ConfigurationHelper.RegisterSettingsFiles(configuration, directory, globalSettingsFile, persistNormalization: false);
 
        return Resolve(configuration, directory);
    }
 
    internal static IReadOnlyList<string> GetConfiguredSources(DirectoryInfo directory, FileInfo globalSettingsFile)
    {
        using var configuration = new ConfigurationManager();
        configuration.AddEnvironmentVariables();
        ConfigurationHelper.RegisterSettingsFiles(configuration, directory, globalSettingsFile, persistNormalization: false);
 
        // Disabling discovery does not release feeds that installed packages may still need.
        var section = configuration.GetSection(SectionName);
        if (section.Value is not null)
        {
            throw InvalidConfiguration(SectionName);
        }
 
        var sources = new List<string>();
        foreach (var entry in section.GetChildren())
        {
            SourceNameValidator.Validate(entry.Key, $"{SectionName}.{entry.Key}");
            if (entry.Value is not null)
            {
                throw InvalidConfiguration($"{SectionName}.{entry.Key}");
            }
            if (entry["source"] is { } source)
            {
                sources.Add(ResolveSource(source, $"{SectionName}.{entry.Key}.source", directory));
            }
        }
 
        return sources.Distinct(PackageSourceIdentity.Comparer).ToArray();
    }
 
    internal static IReadOnlyList<IntegrationSourceConfiguration> Resolve(IConfiguration configuration, DirectoryInfo directory)
    {
        var section = configuration.GetSection(SectionName);
        if (section.Value is not null)
        {
            throw InvalidConfiguration(SectionName);
        }
 
        var sources = new List<IntegrationSourceConfiguration>();
        foreach (var name in new[] { DefaultName }.Concat(section.GetChildren().Select(child => child.Key)).Distinct(StringComparer.OrdinalIgnoreCase))
        {
            var settings = section.GetSection(name);
            var key = $"{SectionName}.{name}";
            SourceNameValidator.Validate(name, key);
            if (settings.Value is not null)
            {
                throw InvalidConfiguration(key);
            }
            if (settings.GetBool("enabled") is false)
            {
                continue;
            }
 
            var source = settings["source"];
            var patternsSection = settings.GetSection("patterns");
            var patterns = patternsSection.GetChildren().Select(child => child.Value).ToArray();
            // Explicit "patterns": null or {} must not be treated as an omitted property.
            var hasPatterns = settings.GetChildren().Any(child => child.Key.Equals("patterns", StringComparison.OrdinalIgnoreCase));
            if (source is null && name.Equals(DefaultName, StringComparison.OrdinalIgnoreCase) && !hasPatterns)
            {
                sources.Add(new IntegrationSourceConfiguration());
                continue;
            }
            if (!hasPatterns)
            {
                patterns = ["*"];
            }
            if (patternsSection.Value is not null || patterns.Length == 0 ||
                patternsSection.GetChildren().Where((child, index) => child.Key != index.ToString(CultureInfo.InvariantCulture)).Any() ||
                patterns.Any(pattern => pattern is null || !IsPattern(pattern)))
            {
                throw InvalidConfiguration($"{key}.patterns");
            }
 
            var resolvedSource = ResolveSource(source, $"{key}.source", directory);
            sources.Add(new IntegrationSourceConfiguration
            {
                Name = name,
                Source = resolvedSource,
                Patterns = patterns.Select(pattern => pattern!).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
            });
        }
 
        return
        [
            .. sources.Where(source => source.Source is null),
            .. sources.Where(source => source.Source is not null)
                .GroupBy(source => source.Source!, PackageSourceIdentity.Comparer)
                .Select(group => new IntegrationSourceConfiguration
                {
                    Name = group.First().Name,
                    Source = group.First().Source,
                    Patterns = group.SelectMany(source => source.Patterns!).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
                })
        ];
    }
 
    private static bool IsIdentifier(string value)
        => value.Length > 0 && value.All(c => char.IsAsciiLetterOrDigit(c) || c is '.' or '-' or '_');
 
    private static string ResolveSource(string? source, string key, DirectoryInfo directory)
    {
        if (string.IsNullOrWhiteSpace(source) || PackageSourceOverrideMappings.HasCredentialMaterial(source) ||
            Uri.TryCreate(source, UriKind.Absolute, out var uri) && !uri.IsFile && uri.Scheme is not ("https" or "http"))
        {
            throw InvalidConfiguration(key);
        }
 
        return PackageSourceOverrideMappings.ResolveForWorkingDirectory(source, directory);
    }
 
    private static bool IsPattern(string pattern)
        => pattern == "*" || IsIdentifier(pattern.EndsWith('*') ? pattern[..^1] : pattern);
 
    private static InvalidOperationException InvalidConfiguration(string key)
        => new(string.Format(CultureInfo.CurrentCulture, ErrorStrings.InvalidIntegrationSource, key));
}