File: parent\dotnet\NugetSearch\NugetToolSearchApiRequest.cs
Web Access
Project: dotnet-aot.csproj (dotnet-aot)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.Cli.Commands;
using Microsoft.DotNet.Cli.Commands.Tool.Search;
using Microsoft.DotNet.Cli.ToolPackage;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Packaging.Core;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;

namespace Microsoft.DotNet.Cli.NugetSearch;

internal sealed class NugetToolSearchApiRequest : INugetToolSearchApiRequest
{
    private readonly Func<PackageSource, CancellationToken, Task<PackageSearchResource?>> _getPackageSearchResource;

    public NugetToolSearchApiRequest()
        : this(GetPackageSearchResource)
    {
    }

    internal NugetToolSearchApiRequest(
        Func<PackageSource, CancellationToken, Task<PackageSearchResource?>> getPackageSearchResource)
    {
        _getPackageSearchResource = getPackageSearchResource;
    }

    public async Task<IReadOnlyCollection<SearchResultPackage>> GetResult(
        NugetSearchApiParameter nugetSearchApiParameter,
        PackageSource source,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(nugetSearchApiParameter);
        ArgumentNullException.ThrowIfNull(source);

        using var timeoutCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeoutCancellation.CancelAfter(TimeSpan.FromSeconds(10));

        try
        {
            PackageSearchResource? searchResource = await _getPackageSearchResource(source, timeoutCancellation.Token).ConfigureAwait(false);
            if (searchResource is null)
            {
                throw new NugetSearchApiRequestException(
                    $"The source '{source.Source}' does not provide {nameof(PackageSearchResource)}.");
            }

            if (!searchResource.SupportsPackageTypeFiltering)
            {
                throw new NugetSearchApiRequestException(
                    string.Format(CliCommandStrings.ToolSearchSourceDoesNotSupportPackageTypeFiltering, source.Source));
            }

            var searchFilter = new SearchFilter(nugetSearchApiParameter.Prerelease)
            {
                PackageType = PackageType.DotnetTool.Name,
            };

            IEnumerable<IPackageSearchMetadata> metadata = await searchResource.SearchAsync(
                Uri.EscapeDataString(nugetSearchApiParameter.SearchTerm ?? string.Empty),
                searchFilter,
                nugetSearchApiParameter.Skip ?? 0,
                nugetSearchApiParameter.Take ?? 20,
                NullLogger.Instance,
                timeoutCancellation.Token).ConfigureAwait(false);

            var results = new List<SearchResultPackage>();
            foreach (IPackageSearchMetadata packageMetadata in metadata)
            {
                IEnumerable<VersionInfo> versions = nugetSearchApiParameter.IncludeVersions
                    ? await packageMetadata.GetVersionsAsync().ConfigureAwait(false)
                    : [];
                results.Add(new SearchResultPackage(
                    new PackageId(packageMetadata.Identity.Id),
                    packageMetadata.Identity.Version.ToNormalizedString(),
                    packageMetadata.Description,
                    packageMetadata.Summary,
                    SplitMetadataField(packageMetadata.Tags, splitOnSpaces: true),
                    SplitMetadataField(packageMetadata.Authors, splitOnSpaces: false),
                    packageMetadata.DownloadCount ?? 0,
                    packageMetadata.PrefixReserved,
                    versions.Select(version => new SearchResultPackageVersion(
                        version.Version.ToNormalizedString(),
                        version.DownloadCount ?? 0)).ToArray()));
            }

            return results;
        }
        catch (Exception ex) when (ex is HttpRequestException
            or NuGetProtocolException
            or IOException
            or InvalidOperationException
            or NotSupportedException)
        {
            throw new NugetSearchApiRequestException(ex.Message);
        }
        catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
        {
            throw new NugetSearchApiRequestException(ex.Message);
        }
    }

    private static async Task<PackageSearchResource?> GetPackageSearchResource(
        PackageSource source,
        CancellationToken cancellationToken)
        => await Repository.Factory.GetCoreV3(source)
            .GetResourceAsync<PackageSearchResource>(cancellationToken)
            .ConfigureAwait(false);

    private static string[] SplitMetadataField(string? value, bool splitOnSpaces)
        => string.IsNullOrWhiteSpace(value)
            ? []
            : value.Split(
                splitOnSpaces ? [',', ' '] : [','],
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}