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

Add ReadmeUriTemplateResource for download README from remote sources. #6136

Merged
merged 14 commits into from
Nov 23, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ private void DetailControlModel_PropertyChanged(object sender, PropertyChangedEv
{
NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
if (_readmeTabEnabled)
if (_readmeTabEnabled && e.PropertyName == nameof(DetailControlModel.PackageMetadata))
{
await ReadmePreviewViewModel.SetPackageMetadataAsync(DetailControlModel.PackageMetadata, CancellationToken.None);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public async Task SetPackageMetadataAsync(DetailedPackageMetadata packageMetadat
{
if (packageMetadata != null && (!string.Equals(packageMetadata.Id, _packageMetadata?.Id) || packageMetadata.Version != _packageMetadata?.Version))
{
ReadmeMarkdown = Resources.Text_Loading;
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
_packageMetadata = packageMetadata;
await LoadReadmeAsync(cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Windows;
using System.Windows.Controls;
using Microsoft.VisualStudio.Markdown.Platform;
using Microsoft.VisualStudio.Shell;
using NuGet.PackageManagement.UI.ViewModels;
using NuGet.VisualStudio;
using NuGet.VisualStudio.Telemetry;
Expand Down Expand Up @@ -39,7 +38,7 @@ private void ReadmeViewModel_PropertyChanged(object sender, PropertyChangedEvent
{
if (e.PropertyName == nameof(ReadmePreviewViewModel.ReadmeMarkdown))
{
NuGetUIThreadHelper.JoinableTaskFactory.Run(UpdateMarkdownAsync);
NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(UpdateMarkdownAsync).PostOnFailure(nameof(PackageReadmeControl), nameof(ReadmeViewModel_PropertyChanged));
}
}

Expand Down Expand Up @@ -104,10 +103,8 @@ private void PackageReadmeControl_Unloaded(object sender, RoutedEventArgs e)

private void PackageReadmeControl_Loaded(object sender, RoutedEventArgs e)
{
ThreadHelper.JoinableTaskFactory.Run(async () =>
{
await UpdateMarkdownAsync();
});
NuGetUIThreadHelper.JoinableTaskFactory.RunAsync(UpdateMarkdownAsync)
.PostOnFailure(nameof(PackageReadmeControl), nameof(PackageReadmeControl_Loaded));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public string Owners
public Uri ReadmeUrl { get; private set; }

[JsonProperty(PropertyName = JsonProperties.ReadmeFileUrl)]
public string ReadmeFileUrl { get; private set; }
public string ReadmeFileUrl { get; internal set; }
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved

[JsonIgnore]
public Uri ReportAbuseUrl { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public override async Task<Tuple<bool, INuGetResource>> TryCreate(SourceReposito
{
var regResource = await source.GetResourceAsync<RegistrationResourceV3>(token);
var reportAbuseResource = await source.GetResourceAsync<ReportAbuseResourceV3>(token);
var readmeResource = await source.GetResourceAsync<ReadmeUriTemplateResource>(token);
var packageDetailsUriResource = await source.GetResourceAsync<PackageDetailsUriResourceV3>(token);

var httpSourceResource = await source.GetResourceAsync<HttpSourceResource>(token);
Expand All @@ -32,7 +33,8 @@ public override async Task<Tuple<bool, INuGetResource>> TryCreate(SourceReposito
httpSourceResource.HttpSource,
regResource,
reportAbuseResource,
packageDetailsUriResource);
packageDetailsUriResource,
readmeResource);
}

return new Tuple<bool, INuGetResource>(curResource != null, curResource);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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.

jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
using System;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Protocol.Core.Types;

namespace NuGet.Protocol
{
internal class ReadmeUriTemplateResourceProvider : ResourceProvider
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
{
public ReadmeUriTemplateResourceProvider()
: base(typeof(ReadmeUriTemplateResource),
nameof(ReadmeUriTemplateResource),
NuGetResourceProviderPositions.Last)
{
}

public override async Task<Tuple<bool, INuGetResource>> TryCreate(SourceRepository source, CancellationToken token)
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
{
ReadmeUriTemplateResource resource = null;
var serviceIndex = await source.GetResourceAsync<ServiceIndexResourceV3>(token);
if (serviceIndex != null)
{
var uriTemplate = serviceIndex.GetServiceEntryUri(ServiceTypes.ReadmeFileUrl)?.OriginalString;

// construct a new resource
resource = string.IsNullOrWhiteSpace(uriTemplate) ? null : new ReadmeUriTemplateResource(uriTemplate);
}

return new Tuple<bool, INuGetResource>(resource != null, resource);
}
}
}
1 change: 1 addition & 0 deletions src/NuGet.Core/NuGet.Protocol/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public virtual IEnumerable<Lazy<INuGetResourceProvider>> GetCoreV3()
yield return new Lazy<INuGetResourceProvider>(() => new RegistrationResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new SymbolPackageUpdateResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new ReportAbuseResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new ReadmeUriTemplateResourceProvider());
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
yield return new Lazy<INuGetResourceProvider>(() => new PackageDetailsUriResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new ServiceIndexResourceV3Provider());
yield return new Lazy<INuGetResourceProvider>(() => new ODataServiceDocumentResourceV2Provider());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class PackageMetadataResourceV3 : PackageMetadataResource
{
private readonly RegistrationResourceV3 _regResource;
private readonly ReportAbuseResourceV3 _reportAbuseResource;
private readonly ReadmeUriTemplateResource _readmeUriTemplateResource;
private readonly PackageDetailsUriResourceV3 _packageDetailsUriResource;
private readonly HttpSource _client;

Expand All @@ -36,6 +37,16 @@ public PackageMetadataResourceV3(
_packageDetailsUriResource = packageDetailsUriResource;
}

internal PackageMetadataResourceV3(
HttpSource client,
RegistrationResourceV3 regResource,
ReportAbuseResourceV3 reportAbuseResource,
PackageDetailsUriResourceV3 packageDetailsUriResource,
ReadmeUriTemplateResource readmeResource) : this(client, regResource, reportAbuseResource, packageDetailsUriResource)
{
_readmeUriTemplateResource = readmeResource;
}

/// <param name="packageId">PackageId for package we're looking.</param>
/// <param name="includePrerelease">Whether to include PreRelease versions into result.</param>
/// <param name="includeUnlisted">Whether to include Unlisted versions into result.</param>
Expand Down Expand Up @@ -269,6 +280,10 @@ private void ProcessRegistrationPage(
{
catalogEntry.ReportAbuseUrl = _reportAbuseResource?.GetReportAbuseUrl(catalogEntry.PackageId, catalogEntry.Version);
catalogEntry.PackageDetailsUrl = _packageDetailsUriResource?.GetUri(catalogEntry.PackageId, catalogEntry.Version);
if (string.IsNullOrWhiteSpace(catalogEntry.ReadmeFileUrl))
{
catalogEntry.ReadmeFileUrl = _readmeUriTemplateResource?.GetReadmeUrl(catalogEntry.PackageId, catalogEntry.Version);
}
catalogEntry = metadataCache.GetObject(catalogEntry);
results.Add(catalogEntry);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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.

using NuGet.Protocol.Core.Types;
using NuGet.Versioning;

#if NETCOREAPP
using System;
#endif

namespace NuGet.Protocol
{
internal class ReadmeUriTemplateResource : INuGetResource
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly string _uriTemplate;

public ReadmeUriTemplateResource(string uriTemplate)
{
_uriTemplate = uriTemplate;
}

/// <summary>
/// Get the URL for downloading the readme file.
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
/// <param name="id">The package id</param>
/// <param name="version">The package version</param>
/// <returns>URL to download README, built using the URI template.</returns>
public string GetReadmeUrl(string id, NuGetVersion version)
{
if (_uriTemplate == null)
{
return string.Empty;
}

var uriString = _uriTemplate
#if NETCOREAPP
.Replace("{lower_id}", id.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase)
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
.Replace("{lower_version}", version.ToNormalizedString().ToLowerInvariant(), StringComparison.OrdinalIgnoreCase);
#else
.Replace("{lower_id}", id.ToLowerInvariant())
.Replace("{lower_version}", version.ToNormalizedString().ToLowerInvariant());
jgonz120 marked this conversation as resolved.
Show resolved Hide resolved
#endif

return uriString;
}
}
}
2 changes: 2 additions & 0 deletions src/NuGet.Core/NuGet.Protocol/ServiceTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ public static class ServiceTypes
public static readonly string Version510 = "/5.1.0";
internal const string Version670 = "/6.7.0";
internal const string Version6110 = "/6.11.0";
internal const string Version6130 = "/6.13.0";

public static readonly string[] SearchQueryService = { "SearchQueryService" + Versioned, "SearchQueryService" + Version340, "SearchQueryService" + Version300beta };
public static readonly string[] RegistrationsBaseUrl = { $"RegistrationsBaseUrl{Versioned}", $"RegistrationsBaseUrl{Version360}", $"RegistrationsBaseUrl{Version340}", $"RegistrationsBaseUrl{Version300rc}", $"RegistrationsBaseUrl{Version300beta}", "RegistrationsBaseUrl" };
public static readonly string[] SearchAutocompleteService = { "SearchAutocompleteService" + Versioned, "SearchAutocompleteService" + Version300beta };
public static readonly string[] ReportAbuse = { "ReportAbuseUriTemplate" + Versioned, "ReportAbuseUriTemplate" + Version300 };
internal static readonly string[] ReadmeFileUrl = { "ReadmeUriTemplate" + Versioned, "ReadmeUriTemplate" + Version6130 };
public static readonly string[] PackageDetailsUriTemplate = { "PackageDetailsUriTemplate" + Version510 };
public static readonly string[] LegacyGallery = { "LegacyGallery" + Versioned, "LegacyGallery" + Version200 };
public static readonly string[] PackagePublish = { "PackagePublish" + Versioned, "PackagePublish" + Version200 };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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.

using System;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Newtonsoft.Json.Linq;
using NuGet.Configuration;
using NuGet.Protocol.Core.Types;
using Xunit;

namespace NuGet.Protocol.Tests.Providers
{
public class ReadmeUriTemplateResourceProviderTests
{
private const string ResourceType = "ReadmeUriTemplate/6.13.0";

private readonly PackageSource _packageSource;
private readonly ReadmeUriTemplateResourceProvider _target;

public ReadmeUriTemplateResourceProviderTests()
{
_packageSource = new PackageSource("https://unit.test");
_target = new ReadmeUriTemplateResourceProvider();
}

[Fact]
public async Task TryCreate_WhenResourceDoesNotExist_ReturnsNull()
{
var resourceProviders = new ResourceProvider[]
{
CreateServiceIndexResourceV3Provider(),
_target
};
var sourceRepository = new SourceRepository(_packageSource, resourceProviders);

Tuple<bool, INuGetResource> result = await _target.TryCreate(sourceRepository, CancellationToken.None);

Assert.False(result.Item1);
Assert.Null(result.Item2);
}

[Fact]
public async Task TryCreate_WhenResourceExists_ReturnsValidResourceAsync()
{
var serviceEntry = new RawServiceIndexEntry("https://unit.test/packages/{lower_id}/{lower_version}/readme", ResourceType);
var resourceProviders = new ResourceProvider[]
{
CreateServiceIndexResourceV3Provider(serviceEntry),
_target
};
var sourceRepository = new SourceRepository(_packageSource, resourceProviders);

Tuple<bool, INuGetResource> result = await _target.TryCreate(sourceRepository, CancellationToken.None);

Assert.True(result.Item1);
Assert.IsType<ReadmeUriTemplateResource>(result.Item2);
}

private static ServiceIndexResourceV3Provider CreateServiceIndexResourceV3Provider(params RawServiceIndexEntry[] entries)
{
var provider = new Mock<ServiceIndexResourceV3Provider>();

provider.Setup(x => x.Name)
.Returns(nameof(ServiceIndexResourceV3Provider));
provider.Setup(x => x.ResourceType)
.Returns(typeof(ServiceIndexResourceV3));

var resources = new JArray();

foreach (var entry in entries)
{
resources.Add(
new JObject(
new JProperty("@id", entry.Uri),
new JProperty("@type", entry.Type)));
}

var index = new JObject();

index.Add("version", "3.0.0");
index.Add("resources", resources);
index.Add("@context",
new JObject(
new JProperty("@vocab", "http://schema.nuget.org/schema#"),
new JProperty("comment", "http://www.w3.org/2000/01/rdf-schema#comment")));

var serviceIndexResource = new ServiceIndexResourceV3(index, DateTime.UtcNow);
var tryCreateResult = new Tuple<bool, INuGetResource>(true, serviceIndexResource);

provider.Setup(x => x.TryCreate(It.IsAny<SourceRepository>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(tryCreateResult));

return provider.Object;
}

private class RawServiceIndexEntry
{
public RawServiceIndexEntry(string uri, string type)
{
Uri = uri;
Type = type ?? throw new ArgumentNullException(nameof(type));
}

public string Uri { get; }
public string Type { get; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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.

using NuGet.Versioning;
using Xunit;

namespace NuGet.Protocol.Tests
{
public class ReadmeUriTemplateResourceTests
{
[Theory]
[InlineData("")]
[InlineData(null)]
public void ReadmeUriTemplateResource_GetReadmeUrl_BlankTemplate_ReturnsEmptyString(string uriTemplate)
{
string expectedResult = string.Empty;
var resource = new ReadmeUriTemplateResource(uriTemplate);

var actual = resource.GetReadmeUrl("TestPackage", NuGetVersion.Parse("1.0.0"));

Assert.Equal(expectedResult, actual);
}

[Fact]
public void ReadmeUriTemplateResource_GetReadmeUrl_ReturnsFormedUrl()
{
const string uriTemplate = "https://test.nuget.org/{lower_id}/{lower_version}/readme";
const string expectedResult = "https://test.nuget.org/testpackage/1.0.0/readme";
var resource = new ReadmeUriTemplateResource(uriTemplate);

var actual = resource.GetReadmeUrl("TestPackage", NuGetVersion.Parse("1.0.0"));

Assert.Equal(expectedResult, actual.ToString());
}
}
}
Loading