From 237925d51ebe623e22b8f79877ea0df1d0efcd83 Mon Sep 17 00:00:00 2001 From: Jonas Dellinger Date: Mon, 28 Dec 2020 17:23:59 +0100 Subject: [PATCH] Added paging methods, closes #538 --- .../Clients/SpotifyClientTest.cs | 96 +++++++++++++++++++ .../Clients/Interfaces/ISpotifyClient.cs | 10 ++ SpotifyAPI.Web/Clients/SpotifyClient.cs | 84 ++++++++++++++-- SpotifyAPI.Web/Clients/SpotifyClientConfig.cs | 34 ++++++- .../Exceptions/APIPagingException.cs | 31 ++++++ 5 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 SpotifyAPI.Web.Tests/Clients/SpotifyClientTest.cs create mode 100644 SpotifyAPI.Web/Exceptions/APIPagingException.cs diff --git a/SpotifyAPI.Web.Tests/Clients/SpotifyClientTest.cs b/SpotifyAPI.Web.Tests/Clients/SpotifyClientTest.cs new file mode 100644 index 00000000..6dd4b46d --- /dev/null +++ b/SpotifyAPI.Web.Tests/Clients/SpotifyClientTest.cs @@ -0,0 +1,96 @@ +using System.Reflection; +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web.Tests +{ + [TestFixture] + public class SpotifyClientTest + { + [Test] + public async Task NextPageForIPaginatable() + { + var api = new Mock(); + var config = SpotifyClientConfig.CreateDefault("FakeToken").WithAPIConnector(api.Object); + var spotify = new SpotifyClient(config); + + var response = new SearchResponse + { + Albums = new Paging + { + Next = "https://next-url", + } + }; + + await spotify.NextPage(response.Albums); + api.Verify(a => a.Get(new System.Uri("https://next-url")), Times.Once); + } + + [Test] + public async Task NextPageForCursorPaging() + { + var api = new Mock(); + var config = SpotifyClientConfig.CreateDefault("FakeToken").WithAPIConnector(api.Object); + var spotify = new SpotifyClient(config); + + var response = new CursorPaging + { + Next = "https://next-url" + }; + + await spotify.NextPage(response); + api.Verify(a => a.Get>(new System.Uri("https://next-url")), Times.Once); + } + + [Test] + public async Task NextPageForPaging() + { + var api = new Mock(); + var config = SpotifyClientConfig.CreateDefault("FakeToken").WithAPIConnector(api.Object); + var spotify = new SpotifyClient(config); + + var response = new Paging + { + Next = "https://next-url" + }; + + await spotify.NextPage(response); + api.Verify(a => a.Get>(new System.Uri("https://next-url")), Times.Once); + } + + [Test] + public async Task PreviousPageForPaging() + { + var api = new Mock(); + var config = SpotifyClientConfig.CreateDefault("FakeToken").WithAPIConnector(api.Object); + var spotify = new SpotifyClient(config); + + var response = new Paging + { + Previous = "https://previous-url" + }; + + await spotify.PreviousPage(response); + api.Verify(a => a.Get>(new System.Uri("https://previous-url")), Times.Once); + } + + [Test] + public async Task PreviousPageForCustomPaging() + { + var api = new Mock(); + var config = SpotifyClientConfig.CreateDefault("FakeToken").WithAPIConnector(api.Object); + var spotify = new SpotifyClient(config); + + var response = new Paging + { + Previous = "https://previous-url" + }; + + await spotify.PreviousPage(response); + api.Verify(a => a.Get(new System.Uri("https://previous-url")), Times.Once); + } + } +} diff --git a/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs b/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs index bf16c554..96b68024 100644 --- a/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs +++ b/SpotifyAPI.Web/Clients/Interfaces/ISpotifyClient.cs @@ -167,5 +167,15 @@ namespace SpotifyAPI.Web ); #endif + + public Task> NextPage(Paging paging); + + public Task> NextPage(CursorPaging cursorPaging); + + public Task NextPage(IPaginatable paginatable); + + public Task> PreviousPage(Paging paging); + + public Task PreviousPage(Paging paging); } } diff --git a/SpotifyAPI.Web/Clients/SpotifyClient.cs b/SpotifyAPI.Web/Clients/SpotifyClient.cs index f4266540..05b9f3b3 100644 --- a/SpotifyAPI.Web/Clients/SpotifyClient.cs +++ b/SpotifyAPI.Web/Clients/SpotifyClient.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using SpotifyAPI.Web.Http; using System.Runtime.CompilerServices; +using System.Threading; namespace SpotifyAPI.Web { @@ -29,14 +29,7 @@ namespace SpotifyAPI.Web #pragma warning restore CA2208 } - _apiConnector = new APIConnector( - config.BaseAddress, - config.Authenticator, - config.JSONSerializer, - config.HTTPClient, - config.RetryHandler, - config.HTTPLogger - ); + _apiConnector = config.BuildAPIConnector(); _apiConnector.ResponseReceived += (sender, response) => { LastResponse = response; @@ -124,6 +117,79 @@ namespace SpotifyAPI.Web return (paginator ?? DefaultPaginator).PaginateAll(firstPage, mapper, _apiConnector); } + private Task FetchPage(string? nextUrl) + { + if (nextUrl == null) + { + throw new APIPagingException("The paging object has no next page"); + } + + return _apiConnector.Get(new Uri(nextUrl, UriKind.Absolute)); + } + + /// + /// Fetches the next page of the paging object + /// + /// A paging object which has a next page + /// + /// + public Task> NextPage(Paging paging) + { + Ensure.ArgumentNotNull(paging, nameof(paging)); + return FetchPage>(paging.Next); + } + + /// + /// Fetches the next page of the cursor paging object + /// + /// A cursor paging object which has a next page + /// + /// + public Task> NextPage(CursorPaging cursorPaging) + { + Ensure.ArgumentNotNull(cursorPaging, nameof(cursorPaging)); + return FetchPage>(cursorPaging.Next); + } + + /// + /// Fetches the next page of the complex IPaginatable object. + /// + /// A complex IPaginatable object with a next page + /// + /// The type of the next page + /// + public Task NextPage(IPaginatable paginatable) + { + Ensure.ArgumentNotNull(paginatable, nameof(paginatable)); + return FetchPage(paginatable.Next); + } + + /// + /// Fetches the previous page of the paging object. + /// + /// A paging object with a previous page + /// + /// + public Task> PreviousPage(Paging paging) + { + Ensure.ArgumentNotNull(paging, nameof(paging)); + return FetchPage>(paging.Previous); + } + + + /// + /// Fetches the previous page of the complex paging object. + /// + /// A complex paging object with a previous page + /// + /// The type of the next page + /// + public Task PreviousPage(Paging paging) + { + Ensure.ArgumentNotNull(paging, nameof(paging)); + return FetchPage(paging.Previous); + } + #if NETSTANDARD2_1 /// diff --git a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs index 75bc7881..56a419cd 100644 --- a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs +++ b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs @@ -13,6 +13,7 @@ namespace SpotifyAPI.Web public IHTTPLogger? HTTPLogger { get; private set; } public IRetryHandler? RetryHandler { get; private set; } public IPaginator DefaultPaginator { get; private set; } + public IAPIConnector? APIConnector { get; private set; } /// /// This config spefies the internal parts of the SpotifyClient. @@ -24,6 +25,7 @@ namespace SpotifyAPI.Web /// /// /// + /// public SpotifyClientConfig( Uri baseAddress, IAuthenticator? authenticator, @@ -31,7 +33,8 @@ namespace SpotifyAPI.Web IHTTPClient httpClient, IRetryHandler? retryHandler, IHTTPLogger? httpLogger, - IPaginator defaultPaginator + IPaginator defaultPaginator, + IAPIConnector? apiConnector = null ) { BaseAddress = baseAddress; @@ -41,6 +44,7 @@ namespace SpotifyAPI.Web RetryHandler = retryHandler; HTTPLogger = httpLogger; DefaultPaginator = defaultPaginator; + APIConnector = apiConnector; } public SpotifyClientConfig WithToken(string token, string tokenType = "Bearer") @@ -145,6 +149,34 @@ namespace SpotifyAPI.Web ); } + public SpotifyClientConfig WithAPIConnector(IAPIConnector apiConnector) + { + Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector)); + + return new SpotifyClientConfig( + BaseAddress, + Authenticator, + JSONSerializer, + HTTPClient, + RetryHandler, + HTTPLogger, + DefaultPaginator, + apiConnector + ); + } + + public IAPIConnector BuildAPIConnector() + { + return APIConnector ?? new APIConnector( + BaseAddress, + Authenticator, + JSONSerializer, + HTTPClient, + RetryHandler, + HTTPLogger + ); + } + public static SpotifyClientConfig CreateDefault(string token, string tokenType = "Bearer") { return CreateDefault().WithAuthenticator(new TokenAuthenticator(token, tokenType)); diff --git a/SpotifyAPI.Web/Exceptions/APIPagingException.cs b/SpotifyAPI.Web/Exceptions/APIPagingException.cs new file mode 100644 index 00000000..b5fd5c47 --- /dev/null +++ b/SpotifyAPI.Web/Exceptions/APIPagingException.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using System.Runtime.Serialization; +using System; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + [Serializable] + public class APIPagingException : APIException + { + public TimeSpan RetryAfter { get; } + + public APIPagingException(IResponse response) : base(response) + { + Ensure.ArgumentNotNull(response, nameof(response)); + + if (response.Headers.TryGetValue("Retry-After", out string? retryAfter)) + { + RetryAfter = TimeSpan.FromSeconds(int.Parse(retryAfter, CultureInfo.InvariantCulture)); + } + } + + public APIPagingException() { } + + public APIPagingException(string message) : base(message) { } + + public APIPagingException(string message, Exception innerException) : base(message, innerException) { } + + protected APIPagingException(SerializationInfo info, StreamingContext context) : base(info, context) { } + } +}