Skip to content

Commit

Permalink
Feature: Nuget Author helpers (#1352)
Browse files Browse the repository at this point in the history
* add nuget author helpers

* license tweaks

* Update PackageSourceExtensions.cs

* suppress ca1848 for nuget forwarder for now

* import maths max helper used in mvvm tracking
  • Loading branch information
dpvreony committed May 4, 2023
1 parent 05f0c4e commit dfd75b3
Show file tree
Hide file tree
Showing 12 changed files with 2,414 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/Whipstaff.Core/Entities/IEntityAsType.cs
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
@@ -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
@@ -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
@@ -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
@@ -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;
}
}
}

0 comments on commit dfd75b3

Please sign in to comment.