diff --git a/src/BaGet.Protocol/NuGetClient.cs b/src/BaGet.Protocol/NuGetClient.cs index d193be04..7b3c9964 100644 --- a/src/BaGet.Protocol/NuGetClient.cs +++ b/src/BaGet.Protocol/NuGetClient.cs @@ -19,6 +19,7 @@ public class NuGetClient private readonly IPackageContentClient _contentClient; private readonly IPackageMetadataClient _metadataClient; private readonly ISearchClient _searchClient; + private readonly IAutocompleteClient _autocompleteClient; /// /// Initializes a new instance of the class @@ -48,6 +49,7 @@ public NuGetClient(string serviceIndexUrl) _contentClient = clientFactory.CreatePackageContentClient(); _metadataClient = clientFactory.CreatePackageMetadataClient(); _searchClient = clientFactory.CreateSearchClient(); + _autocompleteClient = clientFactory.CreateAutocompleteClient(); } /// @@ -303,6 +305,33 @@ public virtual async Task> SearchAsync( return response.Data; } + /// + /// Search for packages. Includes prerelease packages. + /// + /// + /// The search query. If , gets default search results. + /// + /// The number of results to skip. + /// The number of results to include. + /// A token to cancel the task. + /// The search results, including prerelease packages. + public virtual async Task> SearchAsync( + string query, + int skip, + int take, + CancellationToken cancellationToken = default) + { + var response = await _searchClient.SearchAsync( + query, + skip, + take, + includePrerelease: true, + includeSemVer2: true, + cancellationToken: cancellationToken); + + return response.Data; + } + /// /// Search for packages. /// @@ -326,53 +355,72 @@ public virtual async Task> SearchAsync( } /// - /// Search for packages. Includes prerelease packages. + /// Search for packages. /// /// /// The search query. If , gets default search results. /// /// The number of results to skip. /// The number of results to include. + /// Whether to include prerelease packages. /// A token to cancel the task. /// The search results, including prerelease packages. public virtual async Task> SearchAsync( string query, int skip, int take, + bool includePrerelease, CancellationToken cancellationToken = default) { - var response = await _searchClient.SearchAsync( + var response = await _searchClient.SearchAsync( query, skip, take, - cancellationToken: cancellationToken); + includePrerelease, + includeSemVer2: true, + cancellationToken); return response.Data; } /// - /// Search for packages. + /// Search for package IDs. Includes prerelease packages. /// /// - /// The search query. If , gets default search results. + /// The search query. If , gets default autocomplete results. + /// + /// A token to cancel the task. + /// The package IDs that matched the query. + public virtual async Task> AutocompleteAsync( + string query = null, + CancellationToken cancellationToken = default) + { + var response = await _autocompleteClient.AutocompleteAsync(query, cancellationToken: cancellationToken); + + return response.Data; + } + + /// + /// Search for package IDs. Includes prerelease packages. + /// + /// + /// The search query. If , gets default autocomplete results. /// - /// Whether to include prerelease packages. /// The number of results to skip. /// The number of results to include. /// A token to cancel the task. - /// The search results, including prerelease packages. - public virtual async Task> SearchAsync( + /// The package IDs that matched the query. + public virtual async Task> AutocompleteAsync( string query, - bool includePrerelease, int skip, int take, CancellationToken cancellationToken = default) { - var response = await _searchClient.SearchAsync( + var response = await _autocompleteClient.AutocompleteAsync( query, skip, take, - includePrerelease, + includePrerelease: true, includeSemVer2: true, cancellationToken); @@ -385,13 +433,18 @@ public virtual async Task> SearchAsync( /// /// The search query. If , gets default autocomplete results. /// + /// Whether to include prerelease packages. /// A token to cancel the task. /// The package IDs that matched the query. public virtual async Task> AutocompleteAsync( - string query = null, + string query, + bool includePrerelease, CancellationToken cancellationToken = default) { - var response = await _searchClient.AutocompleteAsync(query, cancellationToken: cancellationToken); + var response = await _autocompleteClient.AutocompleteAsync( + query, + includePrerelease: includePrerelease, + cancellationToken: cancellationToken); return response.Data; } @@ -404,19 +457,23 @@ public virtual async Task> AutocompleteAsync( /// /// The number of results to skip. /// The number of results to include. + /// Whether to include prerelease packages. /// A token to cancel the task. /// The package IDs that matched the query. public virtual async Task> AutocompleteAsync( string query, int skip, int take, + bool includePrerelease, CancellationToken cancellationToken = default) { - var response = await _searchClient.AutocompleteAsync( + var response = await _autocompleteClient.AutocompleteAsync( query, - skip: skip, - take: take, - cancellationToken: cancellationToken); + skip, + take, + includePrerelease, + includeSemVer2: true, + cancellationToken); return response.Data; } diff --git a/src/BaGet.Protocol/NuGetClientFactory.cs b/src/BaGet.Protocol/NuGetClientFactory.cs index acabd9ff..b6c46a7c 100644 --- a/src/BaGet.Protocol/NuGetClientFactory.cs +++ b/src/BaGet.Protocol/NuGetClientFactory.cs @@ -89,6 +89,17 @@ public virtual ISearchClient CreateSearchClient() return new SearchClient(this); } + /// + /// Create a client to interact with the NuGet Autocomplete resource. + /// + /// See https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource + /// + /// A client to interact with the NuGet Autocomplete resource. + public virtual IAutocompleteClient CreateAutocompleteClient() + { + return new AutocompleteClient(this); + } + /// /// Create a client to interact with the NuGet catalog resource. /// @@ -121,6 +132,11 @@ private Task GetSearchClientAsync(CancellationToken cancellationT return GetAsync(c => c.SearchClient, cancellationToken); } + private Task GetAutocompleteClientAsync(CancellationToken cancellationToken = default) + { + return GetAsync(c => c.AutocompleteClient, cancellationToken); + } + private Task GetCatalogClientAsync(CancellationToken cancellationToken = default) { return GetAsync(c => c.CatalogClient, cancellationToken); @@ -148,7 +164,8 @@ private async Task GetAsync(Func clientFactory, Cancellat var contentClient = new RawPackageContentClient(_httpClient, contentResourceUrl); var metadataClient = new RawPackageMetadataClient(_httpClient, metadataResourceUrl); - var searchClient = new RawSearchClient(_httpClient, searchResourceUrl, autocompleteResourceUrl); + var searchClient = new RawSearchClient(_httpClient, searchResourceUrl); + var autocompleteClient = new RawAutocompleteClient(_httpClient, autocompleteResourceUrl); var catalogClient = catalogResourceUrl == null ? new NullCatalogClient() as ICatalogClient : new RawCatalogClient(_httpClient, catalogResourceUrl); @@ -160,6 +177,7 @@ private async Task GetAsync(Func clientFactory, Cancellat PackageContentClient = contentClient, PackageMetadataClient = metadataClient, SearchClient = searchClient, + AutocompleteClient = autocompleteClient, CatalogClient = catalogClient, }; } @@ -181,6 +199,7 @@ private class NuGetClients public IPackageContentClient PackageContentClient { get; set; } public IPackageMetadataClient PackageMetadataClient { get; set; } public ISearchClient SearchClient { get; set; } + public IAutocompleteClient AutocompleteClient { get; set; } public ICatalogClient CatalogClient { get; set; } } } diff --git a/src/BaGet.Protocol/Search/AutocompleteClient.cs b/src/BaGet.Protocol/Search/AutocompleteClient.cs new file mode 100644 index 00000000..de2c1c43 --- /dev/null +++ b/src/BaGet.Protocol/Search/AutocompleteClient.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BaGet.Protocol.Models; + +namespace BaGet.Protocol +{ + public partial class NuGetClientFactory + { + private class AutocompleteClient : IAutocompleteClient + { + private readonly NuGetClientFactory _clientfactory; + + public AutocompleteClient(NuGetClientFactory clientFactory) + { + _clientfactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); + } + + public async Task AutocompleteAsync( + string query = null, + int skip = 0, + int take = 20, + bool includePrerelease = true, + bool includeSemVer2 = true, + CancellationToken cancellationToken = default) + { + // TODO: Support search failover. + // See: https://github.com/loic-sharma/BaGet/issues/314 + var client = await _clientfactory.GetAutocompleteClientAsync(cancellationToken); + + return await client.AutocompleteAsync(query, skip, take, includePrerelease, includeSemVer2, cancellationToken); + } + + public async Task ListPackageVersionsAsync( + string packageId, + bool includePrerelease = true, + bool includeSemVer2 = true, + CancellationToken cancellationToken = default) + { + // TODO: Support search failover. + // See: https://github.com/loic-sharma/BaGet/issues/314 + var client = await _clientfactory.GetAutocompleteClientAsync(cancellationToken); + + return await client.ListPackageVersionsAsync(packageId, includePrerelease, includeSemVer2, cancellationToken); + } + } + } +} diff --git a/src/BaGet.Protocol/Search/IAutocompleteClient.cs b/src/BaGet.Protocol/Search/IAutocompleteClient.cs new file mode 100644 index 00000000..5afbb4a5 --- /dev/null +++ b/src/BaGet.Protocol/Search/IAutocompleteClient.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using BaGet.Protocol.Models; + +namespace BaGet.Protocol +{ + /// + /// The client used to search for packages. + /// + /// See https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource + /// + public interface IAutocompleteClient + { + /// + /// Perform an autocomplete query on package IDs. + /// See: https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource#search-for-package-ids + /// + /// The autocomplete query. + /// How many results to skip. + /// How many results to return. + /// Whether pre-release packages should be returned. + /// Whether packages that require SemVer 2.0.0 compatibility should be returned. + /// A token to cancel the task. + /// The autocomplete response. + Task AutocompleteAsync( + string query = null, + int skip = 0, + int take = 20, + bool includePrerelease = true, + bool includeSemVer2 = true, + CancellationToken cancellationToken = default); + + /// + /// Enumerate listed package versions. + /// See: https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource#enumerate-package-versions + /// + /// The package ID. + /// Whether pre-release packages should be returned. + /// Whether packages that require SemVer 2.0.0 compatibility should be returned. + /// A token to cancel the task. + /// The package versions that matched the request. + Task ListPackageVersionsAsync( + string packageId, + bool includePrerelease = true, + bool includeSemVer2 = true, + CancellationToken cancellationToken = default); + } +} diff --git a/src/BaGet.Protocol/Search/ISearchClient.cs b/src/BaGet.Protocol/Search/ISearchClient.cs index ccaa691a..6b21029b 100644 --- a/src/BaGet.Protocol/Search/ISearchClient.cs +++ b/src/BaGet.Protocol/Search/ISearchClient.cs @@ -29,26 +29,5 @@ Task SearchAsync( bool includePrerelease = true, bool includeSemVer2 = true, CancellationToken cancellationToken = default); - - /// - /// Perform an autocomplete query. - /// See: https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource - /// - /// The autocomplete query. - /// The autocomplete request type. - /// How many results to skip. - /// How many results to return. - /// Whether pre-release packages should be returned. - /// Whether packages that require SemVer 2.0.0 compatibility should be returned. - /// A token to cancel the task. - /// The autocomplete response. - Task AutocompleteAsync( - string query = null, - AutocompleteType type = AutocompleteType.PackageIds, - int skip = 0, - int take = 20, - bool includePrerelease = true, - bool includeSemVer2 = true, - CancellationToken cancellationToken = default); } } diff --git a/src/BaGet.Protocol/Search/RawAutocompleteClient.cs b/src/BaGet.Protocol/Search/RawAutocompleteClient.cs new file mode 100644 index 00000000..20602a3d --- /dev/null +++ b/src/BaGet.Protocol/Search/RawAutocompleteClient.cs @@ -0,0 +1,72 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BaGet.Protocol.Models; + +namespace BaGet.Protocol.Internal +{ + /// + /// The client used to search for packages. + /// + /// See https://docs.microsoft.com/en-us/nuget/api/search-autocomplete-service-resource + /// + public class RawAutocompleteClient : IAutocompleteClient + { + private readonly HttpClient _httpClient; + private readonly string _autocompleteUrl; + + /// + /// Create a new Search client. + /// + /// The HTTP client used to send requests. + /// The NuGet server's autocomplete URL. + public RawAutocompleteClient(HttpClient httpClient, string autocompleteUrl) + { + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _autocompleteUrl = autocompleteUrl ?? throw new ArgumentNullException(nameof(autocompleteUrl)); + } + + public async Task AutocompleteAsync( + string query = null, + int skip = 0, + int take = 20, + bool includePrerelease = true, + bool includeSemVer2 = true, + CancellationToken cancellationToken = default) + { + var url = RawSearchClient.AddSearchQueryString( + _autocompleteUrl, + query, + skip, + take, + includePrerelease, + includeSemVer2, + "q"); + + var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); + + return response.GetResultOrThrow(); + } + + public async Task ListPackageVersionsAsync( + string packageId, + bool includePrerelease = true, + bool includeSemVer2 = true, + CancellationToken cancellationToken = default) + { + var url = RawSearchClient.AddSearchQueryString( + _autocompleteUrl, + packageId, + skip: null, + take: null, + includePrerelease, + includeSemVer2, + "id"); + + var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); + + return response.GetResultOrThrow(); + } + } +} diff --git a/src/BaGet.Protocol/Search/RawSearchClient.cs b/src/BaGet.Protocol/Search/RawSearchClient.cs index 9bc19a9f..077bd303 100644 --- a/src/BaGet.Protocol/Search/RawSearchClient.cs +++ b/src/BaGet.Protocol/Search/RawSearchClient.cs @@ -18,36 +18,16 @@ public class RawSearchClient : ISearchClient { private readonly HttpClient _httpClient; private readonly string _searchUrl; - private readonly string _autocompleteUrl; /// /// Create a new Search client. /// /// The HTTP client used to send requests. /// The NuGet server's search URL. - /// The NuGet server's autocomplete URL. - public RawSearchClient(HttpClient httpClient, string searchUrl, string autocompleteUrl) + public RawSearchClient(HttpClient httpClient, string searchUrl) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _searchUrl = searchUrl ?? throw new ArgumentNullException(nameof(searchUrl)); - _autocompleteUrl = autocompleteUrl ?? throw new ArgumentNullException(nameof(autocompleteUrl)); - } - - public async Task AutocompleteAsync( - string query = null, - AutocompleteType type = AutocompleteType.PackageIds, - int skip = 0, - int take = 20, - bool includePrerelease = true, - bool includeSemVer2 = true, - CancellationToken cancellationToken = default) - { - var param = (type == AutocompleteType.PackageIds) ? "q" : "id"; - var url = AddSearchQueryString(_autocompleteUrl, query, skip, take, includePrerelease, includeSemVer2, param); - - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - - return response.GetResultOrThrow(); } public async Task SearchAsync( @@ -65,19 +45,19 @@ public async Task SearchAsync( return response.GetResultOrThrow(); } - private string AddSearchQueryString( + internal static string AddSearchQueryString( string uri, string query, - int skip, - int take, + int? skip, + int? take, bool includePrerelease, bool includeSemVer2, string queryParamName) { var queryString = new Dictionary(); - if (skip != 0) queryString["skip"] = skip.ToString(); - if (take != 0) queryString["take"] = take.ToString(); + if (skip.HasValue && skip.Value > 0) queryString["skip"] = skip.ToString(); + if (take.HasValue) queryString["take"] = take.ToString(); if (includePrerelease) queryString["prerelease"] = true.ToString(); if (includeSemVer2) queryString["semVerLevel"] = "2.0.0"; @@ -90,7 +70,7 @@ private string AddSearchQueryString( } // See: https://github.com/aspnet/AspNetCore/blob/8c02467b4a218df3b1b0a69bceb50f5b64f482b1/src/Http/WebUtilities/src/QueryHelpers.cs#L63 - private string AddQueryString(string uri, Dictionary queryString) + private static string AddQueryString(string uri, Dictionary queryString) { if (uri.IndexOf('#') != -1) throw new InvalidOperationException("URL anchors are not supported"); if (uri.IndexOf('?') != -1) throw new InvalidOperationException("Adding query strings to URL with query strings is not supported"); diff --git a/src/BaGet.Protocol/Search/SearchClient.cs b/src/BaGet.Protocol/Search/SearchClient.cs index 035f58e2..b6bca0d9 100644 --- a/src/BaGet.Protocol/Search/SearchClient.cs +++ b/src/BaGet.Protocol/Search/SearchClient.cs @@ -16,22 +16,6 @@ public SearchClient(NuGetClientFactory clientFactory) _clientfactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); } - public async Task AutocompleteAsync( - string query = null, - AutocompleteType type = AutocompleteType.PackageIds, - int skip = 0, - int take = 20, - bool includePrerelease = true, - bool includeSemVer2 = true, - CancellationToken cancellationToken = default) - { - // TODO: Support search failover. - // See: https://github.com/loic-sharma/BaGet/issues/314 - var client = await _clientfactory.GetSearchClientAsync(cancellationToken); - - return await client.AutocompleteAsync(query, type, skip, take, includePrerelease, includeSemVer2); - } - public async Task SearchAsync( string query = null, int skip = 0, diff --git a/tests/BaGet.Protocol.Tests/RawAutocompleteClientTests.cs b/tests/BaGet.Protocol.Tests/RawAutocompleteClientTests.cs new file mode 100644 index 00000000..e1171dc6 --- /dev/null +++ b/tests/BaGet.Protocol.Tests/RawAutocompleteClientTests.cs @@ -0,0 +1,64 @@ +using System.Threading.Tasks; +using BaGet.Protocol.Internal; +using Xunit; + +namespace BaGet.Protocol.Tests +{ + public class RawAutocompleteClientTests : IClassFixture + { + private readonly RawAutocompleteClient _target; + + public RawAutocompleteClientTests(ProtocolFixture fixture) + { + _target = fixture.AutocompleteClient; + } + + [Fact] + public async Task GetDefaultAutocompleteResults() + { + var response = await _target.AutocompleteAsync(); + + Assert.NotNull(response); + Assert.Equal(1, response.TotalHits); + Assert.Equal("Test.Package", Assert.Single(response.Data)); + } + + [Fact] + public async Task AddsAutocompleteParameters() + { + await Task.Yield(); + + // TODO: Assert request URL query parameters. + // var response = await _target.AutocompleteAsync( + // "query", + // skip: 2, + // take: 5, + // includePrerelease: false, + // includeSemVer2: false); + } + + [Fact] + public async Task ListsPackageVersions() + { + await Task.Yield(); + + // var response = await _target.ListPackageVersionsAsync("PackageId"); + + // Assert.NotNull(response); + // Assert.Equal(1, response.TotalHits); + // Assert.Equal("1.0.0", response.Data[0]); + } + + [Fact] + public async Task AddsListPackageVersionsParameters() + { + await Task.Yield(); + + // TODO: Assert request URL query parameters. + // var response = await _target.ListPackageVersionsAsync( + // "query", + // includePrerelease: false, + // includeSemVer2: false); + } + } +} diff --git a/tests/BaGet.Protocol.Tests/RawSearchClientTests.cs b/tests/BaGet.Protocol.Tests/RawSearchClientTests.cs index 90a5f8bf..5d2eb84e 100644 --- a/tests/BaGet.Protocol.Tests/RawSearchClientTests.cs +++ b/tests/BaGet.Protocol.Tests/RawSearchClientTests.cs @@ -28,13 +28,17 @@ public async Task GetDefaultSearchResults() } [Fact] - public async Task GetDefaultAutocompleteResults() + public async Task AddsParameters() { - var response = await _target.AutocompleteAsync(); + await Task.Yield(); - Assert.NotNull(response); - Assert.Equal(1, response.TotalHits); - Assert.Equal("Test.Package", Assert.Single(response.Data)); + // TODO: Assert request URL query parameters. + // var response = await _target.SearchAsync( + // "query", + // skip: 2, + // take: 5, + // includePrerelease: false, + // includeSemVer2: false); } } } diff --git a/tests/BaGet.Protocol.Tests/Support/ProtocolFixture.cs b/tests/BaGet.Protocol.Tests/Support/ProtocolFixture.cs index 80146004..cd790587 100644 --- a/tests/BaGet.Protocol.Tests/Support/ProtocolFixture.cs +++ b/tests/BaGet.Protocol.Tests/Support/ProtocolFixture.cs @@ -16,11 +16,9 @@ public ProtocolFixture() ServiceIndexClient = new RawServiceIndexClient(httpClient, TestData.ServiceIndexUrl); ContentClient = new RawPackageContentClient(httpClient, TestData.PackageContentUrl); MetadataClient = new RawPackageMetadataClient(httpClient, TestData.PackageMetadataUrl); + SearchClient = new RawSearchClient(httpClient, TestData.SearchUrl); + AutocompleteClient = new RawAutocompleteClient(httpClient, TestData.AutocompleteUrl); CatalogClient = new RawCatalogClient(httpClient, TestData.CatalogIndexUrl); - SearchClient = new RawSearchClient( - httpClient, - TestData.SearchUrl, - TestData.AutocompleteUrl); } public NuGetClient NuGetClient { get; } @@ -30,6 +28,7 @@ public ProtocolFixture() public RawPackageContentClient ContentClient { get; } public RawPackageMetadataClient MetadataClient { get; } public RawSearchClient SearchClient { get; } + public RawAutocompleteClient AutocompleteClient { get; } public RawCatalogClient CatalogClient { get; } } }