Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Nuget Author helpers #1352

Merged
merged 8 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Whipstaff.Core/Entities/IEntityAsType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Whipstaff.Core.Entities
/// and easier to track changes on.
/// </summary>
/// <typeparam name="TValue">Type for the wrapped value.</typeparam>
public interface IEntityAsType<TValue>
public interface IEntityAsType<out TValue>
{
/// <summary>
/// Gets the wrapped value.
Expand Down
34 changes: 34 additions & 0 deletions src/Whipstaff.Nuget/AuthorUsernameAsStringModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using Whipstaff.Core.Entities;
using Whipstaff.Runtime.Extensions;

namespace Whipstaff.Nuget
{
/// <summary>
/// Represents a nuget author username.
/// </summary>
public sealed class AuthorUsernameAsStringModel : IEntityAsString
{
/// <summary>
/// Initializes a new instance of the <see cref="AuthorUsernameAsStringModel"/> class.
/// </summary>
/// <param name="value">Nuget username as a string.</param>
public AuthorUsernameAsStringModel(string value)
{
value.ThrowIfNullOrWhitespace();
if (!value.IsAsciiLettersOrNumbers())
{
throw new ArgumentException("Author name must be ASCII letters or numbers", nameof(value));
}

Value = value;
}

/// <inheritdoc/>
public string Value { get; }
}
}
90 changes: 90 additions & 0 deletions src/Whipstaff.Nuget/NugetClientHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;

namespace Whipstaff.Nuget
{
/// <summary>
/// Helpers for working with the NuGet client API.
/// </summary>
public static class NugetClientHelpers
{
/// <summary>
/// Gets a list of packages for the given author name.
/// </summary>
/// <param name="authorName">Username of the author.</param>
/// <param name="nugetForwardingToNetCoreLogger">Forwarding logger instance.</param>
/// <param name="cancellationToken">The cancellation token for the operation.</param>
/// <returns>List of packages for the author.</returns>
public static async Task<IList<IPackageSearchMetadata>> GetPackagesForAuthor(
AuthorUsernameAsStringModel authorName,
NugetForwardingToNetCoreLogger nugetForwardingToNetCoreLogger,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(authorName);
ArgumentNullException.ThrowIfNull(nugetForwardingToNetCoreLogger);

var packageSourceProvider = new PackageSourceProvider(new Settings(Environment.CurrentDirectory));
var sources = packageSourceProvider.LoadPackageSources();
var packages = new List<IPackageSearchMetadata>();

foreach (var packageSource in sources)
{
var packagesForAuthor = await packageSource.GetPackagesForAuthor(
authorName,
nugetForwardingToNetCoreLogger,
cancellationToken)
.ConfigureAwait(false);

packages.AddRange(packagesForAuthor);
}

return packages;
}

/// <summary>
/// Gets a list of selected information per package for the given author name.
/// </summary>
/// <typeparam name="TResult">Return type for the selected information.</typeparam>
/// <param name="authorName">Username of the author.</param>
/// <param name="selector">selection predicate of the data to return.</param>
/// <param name="nugetForwardingToNetCoreLogger">Forwarding logger instance.</param>
/// <param name="cancellationToken">The cancellation token for the operation.</param>
/// <returns>List of selected output based on packages for the author.</returns>
public static async Task<IList<TResult>> GetPackagesForAuthor<TResult>(
AuthorUsernameAsStringModel authorName,
Func<IPackageSearchMetadata, TResult> selector,
NugetForwardingToNetCoreLogger nugetForwardingToNetCoreLogger,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(selector);

var packageSourceProvider = new PackageSourceProvider(new Settings(Environment.CurrentDirectory));
var sources = packageSourceProvider.LoadPackageSources();
var packages = new List<TResult>();

foreach (var packageSource in sources)
{
var packagesForAuthor = await packageSource.GetPackagesForAuthor(
authorName,
selector,
nugetForwardingToNetCoreLogger,
cancellationToken)
.ConfigureAwait(false);

packages.AddRange(packagesForAuthor);
}

return packages;
}
}
}
142 changes: 142 additions & 0 deletions src/Whipstaff.Nuget/NugetForwardingToNetCoreLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NuGet.Common;

namespace Whipstaff.Nuget
{
/// <summary>
/// Nuget logger that forwards to a .NET Core logger.
/// </summary>
/// <remarks>
/// This is based upon https://raw.githubusercontent.com/NuGet/NuGet.Jobs/943076ea59f3bad50b019c27e511bf82808575aa/src/NuGet.Services.Metadata.Catalog.Monitoring/Utility/CommonLogger.cs
/// Copyright (c) .NET Foundation. All rights reserved.
/// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
/// </remarks>
public sealed class NugetForwardingToNetCoreLogger : NuGet.Common.ILogger
{
// This event ID is believed to be unused anywhere else but is otherwise arbitrary.
private const int DefaultLogEventId = 23847;
private static readonly EventId DefaultClientLogEvent = new EventId(DefaultLogEventId);
private readonly Microsoft.Extensions.Logging.ILogger _internalLogger;

/// <summary>
/// Initializes a new instance of the <see cref="NugetForwardingToNetCoreLogger"/> class.
/// </summary>
/// <param name="logger">Net Core logging framework instance.</param>
public NugetForwardingToNetCoreLogger(Microsoft.Extensions.Logging.ILogger logger)
{
_internalLogger = logger ?? throw new ArgumentNullException(nameof(logger));
}

/// <inheritdoc/>
public void LogDebug(string data)
{
#pragma warning disable CA1848 // Use the LoggerMessage delegates
_internalLogger.LogDebug("{Data}", data);
#pragma warning restore CA1848 // Use the LoggerMessage delegates
}

/// <inheritdoc/>
public void LogVerbose(string data)
{
#pragma warning disable CA1848 // Use the LoggerMessage delegates
_internalLogger.LogInformation("{Data}", data);
#pragma warning restore CA1848 // Use the LoggerMessage delegates
}

/// <inheritdoc/>
public void LogInformation(string data)
{
#pragma warning disable CA1848 // Use the LoggerMessage delegates
_internalLogger.LogInformation("{Data}", data);
#pragma warning restore CA1848 // Use the LoggerMessage delegates
}

/// <inheritdoc/>
public void LogMinimal(string data)
{
#pragma warning disable CA1848 // Use the LoggerMessage delegates
_internalLogger.LogInformation("{Data}", data);
#pragma warning restore CA1848 // Use the LoggerMessage delegates
}

/// <inheritdoc/>
public void LogWarning(string data)
{
#pragma warning disable CA1848 // Use the LoggerMessage delegates
_internalLogger.LogWarning("{Data}", data);
#pragma warning restore CA1848 // Use the LoggerMessage delegates
}

/// <inheritdoc/>
public void LogError(string data)
{
#pragma warning disable CA1848 // Use the LoggerMessage delegates
_internalLogger.LogError("{Data}", data);
#pragma warning restore CA1848 // Use the LoggerMessage delegates
}

/// <inheritdoc/>
public void LogInformationSummary(string data)
{
#pragma warning disable CA1848 // Use the LoggerMessage delegates
_internalLogger.LogInformation("{Data}", data);
#pragma warning restore CA1848 // Use the LoggerMessage delegates
}

/// <inheritdoc/>
public void Log(NuGet.Common.LogLevel level, string data)
{
_internalLogger.Log(GetLogLevel(level), DefaultClientLogEvent, data, null, (str, ex) => str);
}

/// <inheritdoc/>
public Task LogAsync(NuGet.Common.LogLevel level, string data)
{
_internalLogger.Log(GetLogLevel(level), DefaultClientLogEvent, data, null, (str, ex) => str);
return Task.CompletedTask;
}

/// <inheritdoc/>
public void Log(ILogMessage message)
{
if (message == null)
{
return;
}

_internalLogger.Log(GetLogLevel(message.Level), new EventId((int)message.Code), message.Message, null, (str, ex) => str);
}

/// <inheritdoc/>
public Task LogAsync(ILogMessage message)
{
if (message == null)
{
return Task.CompletedTask;
}

_internalLogger.Log(GetLogLevel(message.Level), new EventId((int)message.Code), message.Message, null, (str, ex) => str);
return Task.CompletedTask;
}

private static Microsoft.Extensions.Logging.LogLevel GetLogLevel(NuGet.Common.LogLevel logLevel)
{
return logLevel switch
{
NuGet.Common.LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
NuGet.Common.LogLevel.Verbose => Microsoft.Extensions.Logging.LogLevel.Information,
NuGet.Common.LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information,
NuGet.Common.LogLevel.Minimal => Microsoft.Extensions.Logging.LogLevel.Information,
NuGet.Common.LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning,
NuGet.Common.LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
_ => Microsoft.Extensions.Logging.LogLevel.None,
};
}
}
}
118 changes: 118 additions & 0 deletions src/Whipstaff.Nuget/PackageSearchResourceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Protocol.Core.Types;

namespace Whipstaff.Nuget
{
/// <summary>
/// Extension methods for <see cref="PackageSearchResource" />.
/// </summary>
public static class PackageSearchResourceExtensions
{
/// <summary>
/// Gets a list of packages for the given author name in the specific package search resource.
/// </summary>
/// <param name="packageSearchResource">Package search resource to check.</param>
/// <param name="authorName">Username of the author.</param>
/// <param name="logger">NuGet logging framework instance.</param>
/// <param name="cancellationToken">The cancellation token for the operation.</param>
/// <returns>List of packages for the author.</returns>
public static async Task<IList<IPackageSearchMetadata>> GetPackagesForAuthor(
this PackageSearchResource packageSearchResource,
AuthorUsernameAsStringModel authorName,
ILogger logger,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(packageSearchResource);
ArgumentNullException.ThrowIfNull(authorName);

var result = new List<IPackageSearchMetadata>();
var searchTerm = $"author:{authorName.Value}";
var searchFilter = new SearchFilter(false);

var packageCount = 0;
var skip = 0;

do
{
var searchResult = await packageSearchResource.SearchAsync(
searchTerm,
searchFilter,
skip: skip,
take: 1000,
logger,
cancellationToken).ConfigureAwait(false);

var packages = searchResult.ToList();
packageCount = packages.Count;

result.AddRange(packages);
skip += 1000;
}
while (packageCount == 1000);

return result;
}

/// <summary>
/// Gets a list of selected information per package for the given author name in the specific package search resource.
/// </summary>
/// <typeparam name="TResult">Return type for the selected information.</typeparam>
/// <param name="packageSearchResource">Package search resource to check.</param>
/// <param name="authorName">Username of the author.</param>
/// <param name="selector">selection predicate of the data to return.</param>
/// <param name="logger">NuGet logging framework instance.</param>
/// <param name="cancellationToken">The cancellation token for the operation.</param>
/// <returns>List of packages for the author.</returns>
public static async Task<IList<TResult>> GetPackagesForAuthor<TResult>(
this PackageSearchResource packageSearchResource,
AuthorUsernameAsStringModel authorName,
Func<IPackageSearchMetadata, TResult> selector,
ILogger logger,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(packageSearchResource);
ArgumentNullException.ThrowIfNull(authorName);
ArgumentNullException.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(logger);

var result = new List<TResult>();
var searchTerm = $"author:{authorName.Value}";
var searchFilter = new SearchFilter(false);

var packageCount = 0;

var skip = 0;

do
{
var searchResult = await packageSearchResource.SearchAsync(
searchTerm,
searchFilter,
skip: 0,
take: 1000,
logger,
cancellationToken).ConfigureAwait(false);

var packages = searchResult.Select(selector)
.ToList();

packageCount = packages.Count;
result.AddRange(packages);

skip += 1000;
}
while (packageCount == 1000);

return result;
}
}
}
Loading