Added Retry Handler and removed old enums

This commit is contained in:
Jonas Dellinger 2020-05-02 19:57:31 +02:00
parent 221d7534fd
commit be7bdb6a93
15 changed files with 143 additions and 257 deletions

View File

@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using SpotifyAPI.Web.Enums;
namespace SpotifyAPI.Web.Examples.ASP namespace SpotifyAPI.Web.Examples.ASP
{ {
@ -26,8 +25,8 @@ namespace SpotifyAPI.Web.Examples.ASP
.AddCookie() .AddCookie()
.AddSpotify(options => .AddSpotify(options =>
{ {
var scopes = Scope.UserLibraryRead | Scope.UserModifyPlaybackState; // var scopes = Scope.UserLibraryRead | Scope.UserModifyPlaybackState;
options.Scope.Add(scopes.GetStringAttribute(",")); // options.Scope.Add(scopes.GetStringAttribute(","));
options.SaveTokens = true; options.SaveTokens = true;
options.ClientId = Configuration["client_id"]; options.ClientId = Configuration["client_id"];

View File

@ -0,0 +1,95 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using SpotifyAPI.Web.Http;
namespace SpotifyAPI.Web.Tests
{
[TestFixture]
public class APIConnectorTest
{
[Test]
public async Task RetryHandler_IsUsed()
{
var apiResponse = new Mock<IAPIResponse<string>>();
apiResponse.SetupGet(a => a.Body).Returns("Hello World");
var response = new Mock<IResponse>();
response.SetupGet(r => r.ContentType).Returns("application/json");
response.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.OK);
response.SetupGet(r => r.Body).Returns("\"Hello World\"");
var authenticator = new Mock<IAuthenticator>();
var serializer = new Mock<IJSONSerializer>();
serializer.Setup(s => s.DeserializeResponse<string>(It.IsAny<IResponse>())).Returns(apiResponse.Object);
var httpClient = new Mock<IHTTPClient>();
var retryHandler = new Mock<IRetryHandler>();
retryHandler.Setup(r =>
r.HandleRetry(
It.IsAny<IRequest>(),
It.IsAny<IResponse>(),
It.IsAny<Func<IRequest, Task<IResponse>>>()
)
).Returns(Task.FromResult(response.Object));
var apiConnector = new APIConnector(
new Uri("https://spotify.com"),
authenticator.Object,
serializer.Object,
httpClient.Object,
retryHandler.Object
);
await apiConnector.SendAPIRequest<string>(new Uri("/me", UriKind.Relative), HttpMethod.Get);
authenticator.Verify(a => a.Apply(It.IsAny<IRequest>()), Times.Once);
httpClient.Verify(h => h.DoRequest(It.IsAny<IRequest>()), Times.Once);
serializer.Verify(s => s.DeserializeResponse<string>(response.Object), Times.Once);
}
[Test]
public async Task RetryHandler_CanRetry()
{
var apiResponse = new Mock<IAPIResponse<string>>();
apiResponse.SetupGet(a => a.Body).Returns("Hello World");
var response = new Mock<IResponse>();
response.SetupGet(r => r.ContentType).Returns("application/json");
response.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.OK);
response.SetupGet(r => r.Body).Returns("\"Hello World\"");
var authenticator = new Mock<IAuthenticator>();
var serializer = new Mock<IJSONSerializer>();
serializer.Setup(s => s.DeserializeResponse<string>(It.IsAny<IResponse>())).Returns(apiResponse.Object);
var httpClient = new Mock<IHTTPClient>();
httpClient.Setup(h => h.DoRequest(It.IsAny<IRequest>())).Returns(Task.FromResult(response.Object));
var retryHandler = new Mock<IRetryHandler>();
retryHandler.Setup(r =>
r.HandleRetry(
It.IsAny<IRequest>(),
It.IsAny<IResponse>(),
It.IsAny<Func<IRequest, Task<IResponse>>>()
)
).Returns((IRequest request, IResponse response, Func<IRequest, Task<IResponse>> retry) => retry(request));
var apiConnector = new APIConnector(
new Uri("https://spotify.com"),
authenticator.Object,
serializer.Object,
httpClient.Object,
retryHandler.Object
);
await apiConnector.SendAPIRequest<string>(new Uri("/me", UriKind.Relative), HttpMethod.Get);
serializer.Verify(s => s.SerializeRequest(It.IsAny<IRequest>()), Times.Once);
authenticator.Verify(a => a.Apply(It.IsAny<IRequest>()), Times.Exactly(2));
httpClient.Verify(h => h.DoRequest(It.IsAny<IRequest>()), Times.Exactly(2));
serializer.Verify(s => s.DeserializeResponse<string>(response.Object), Times.Once);
}
}
}

View File

@ -1,18 +0,0 @@
using System;
using NUnit.Framework;
namespace SpotifyAPI.Web.Tests
{
[TestFixture]
public class UtilTest
{
[Test]
public void TimestampShouldBeNoFloatingPoint()
{
string timestamp = DateTime.Now.ToUnixTimeMillisecondsPoly().ToString();
StringAssert.DoesNotContain(".", timestamp);
StringAssert.DoesNotContain(",", timestamp);
}
}
}

View File

@ -9,6 +9,7 @@ namespace SpotifyAPI.Web
public IAuthenticator Authenticator { get; } public IAuthenticator Authenticator { get; }
public IJSONSerializer JSONSerializer { get; } public IJSONSerializer JSONSerializer { get; }
public IHTTPClient HTTPClient { get; } public IHTTPClient HTTPClient { get; }
public IRetryHandler RetryHandler { get; }
/// <summary> /// <summary>
/// This config spefies the internal parts of the SpotifyClient. /// This config spefies the internal parts of the SpotifyClient.
@ -19,17 +20,20 @@ namespace SpotifyAPI.Web
/// <param name="authenticator"></param> /// <param name="authenticator"></param>
/// <param name="jsonSerializer"></param> /// <param name="jsonSerializer"></param>
/// <param name="httpClient"></param> /// <param name="httpClient"></param>
/// <param name="retryHandler"></param>
public SpotifyClientConfig( public SpotifyClientConfig(
Uri baseAddress, Uri baseAddress,
IAuthenticator authenticator, IAuthenticator authenticator,
IJSONSerializer jsonSerializer, IJSONSerializer jsonSerializer,
IHTTPClient httpClient IHTTPClient httpClient,
IRetryHandler retryHandler
) )
{ {
BaseAddress = baseAddress; BaseAddress = baseAddress;
Authenticator = authenticator; Authenticator = authenticator;
JSONSerializer = jsonSerializer; JSONSerializer = jsonSerializer;
HTTPClient = httpClient; HTTPClient = httpClient;
RetryHandler = retryHandler;
} }
internal IAPIConnector CreateAPIConnector() internal IAPIConnector CreateAPIConnector()
@ -40,7 +44,7 @@ namespace SpotifyAPI.Web
Ensure.PropertyNotNull(JSONSerializer, nameof(JSONSerializer)); Ensure.PropertyNotNull(JSONSerializer, nameof(JSONSerializer));
Ensure.PropertyNotNull(HTTPClient, nameof(HTTPClient)); Ensure.PropertyNotNull(HTTPClient, nameof(HTTPClient));
return new APIConnector(BaseAddress, Authenticator, JSONSerializer, HTTPClient); return new APIConnector(BaseAddress, Authenticator, JSONSerializer, HTTPClient, RetryHandler);
} }
public SpotifyClientConfig WithToken(string token, string tokenType = "Bearer") public SpotifyClientConfig WithToken(string token, string tokenType = "Bearer")
@ -50,11 +54,14 @@ namespace SpotifyAPI.Web
return WithAuthenticator(new TokenHeaderAuthenticator(token, tokenType)); return WithAuthenticator(new TokenHeaderAuthenticator(token, tokenType));
} }
public SpotifyClientConfig WithRetryHandler(IRetryHandler retryHandler)
{
return new SpotifyClientConfig(BaseAddress, Authenticator, JSONSerializer, HTTPClient, retryHandler);
}
public SpotifyClientConfig WithAuthenticator(IAuthenticator authenticator) public SpotifyClientConfig WithAuthenticator(IAuthenticator authenticator)
{ {
Ensure.ArgumentNotNull(authenticator, nameof(authenticator)); return new SpotifyClientConfig(BaseAddress, authenticator, JSONSerializer, HTTPClient, RetryHandler);
return new SpotifyClientConfig(BaseAddress, Authenticator, JSONSerializer, HTTPClient);
} }
public static SpotifyClientConfig CreateDefault(string token, string tokenType = "Bearer") public static SpotifyClientConfig CreateDefault(string token, string tokenType = "Bearer")
@ -79,7 +86,8 @@ namespace SpotifyAPI.Web
SpotifyUrls.API_V1, SpotifyUrls.API_V1,
authenticator, authenticator,
new NewtonsoftJSONSerializer(), new NewtonsoftJSONSerializer(),
new NetHttpClient() new NetHttpClient(),
null
); );
} }
} }

View File

@ -1,23 +0,0 @@
using System;
namespace SpotifyAPI.Web.Enums
{
[Flags]
public enum AlbumType
{
[String("album")]
Album = 1,
[String("single")]
Single = 2,
[String("compilation")]
Compilation = 4,
[String("appears_on")]
AppearsOn = 8,
[String("album,single,compilation,appears_on")]
All = 16
}
}

View File

@ -1,14 +0,0 @@
using System;
namespace SpotifyAPI.Web.Enums
{
[Flags]
public enum FollowType
{
[String("artist")]
Artist = 1,
[String("user")]
User = 2
}
}

View File

@ -1,17 +0,0 @@
using System;
namespace SpotifyAPI.Web.Enums
{
[Flags]
public enum RepeatState
{
[String("track")]
Track = 1,
[String("context")]
Context = 2,
[String("off")]
Off = 4
}
}

View File

@ -1,68 +0,0 @@
using System;
namespace SpotifyAPI.Web.Enums
{
[Flags]
public enum Scope
{
[String("")]
None = 1,
[String("playlist-modify-public")]
PlaylistModifyPublic = 2,
[String("playlist-modify-private")]
PlaylistModifyPrivate = 4,
[String("playlist-read-private")]
PlaylistReadPrivate = 8,
[String("streaming")]
Streaming = 16,
[String("user-read-private")]
UserReadPrivate = 32,
[String("user-read-email")]
UserReadEmail = 64,
[String("user-library-read")]
UserLibraryRead = 128,
[String("user-library-modify")]
UserLibraryModify = 256,
[String("user-follow-modify")]
UserFollowModify = 512,
[String("user-follow-read")]
UserFollowRead = 1024,
[String("user-read-birthdate")]
UserReadBirthdate = 2048,
[String("user-top-read")]
UserTopRead = 4096,
[String("playlist-read-collaborative")]
PlaylistReadCollaborative = 8192,
[String("user-read-recently-played")]
UserReadRecentlyPlayed = 16384,
[String("user-read-playback-state")]
UserReadPlaybackState = 32768,
[String("user-modify-playback-state")]
UserModifyPlaybackState = 65536,
[String("user-read-currently-playing")]
UserReadCurrentlyPlaying = 131072,
[String("app-remote-control")]
AppRemoteControl = 262144,
[String("ugc-image-upload")]
UgcImageUpload = 524288
}
}

View File

@ -1,23 +0,0 @@
using System;
namespace SpotifyAPI.Web.Enums
{
[Flags]
public enum SearchType
{
[String("artist")]
Artist = 1,
[String("album")]
Album = 2,
[String("track")]
Track = 4,
[String("playlist")]
Playlist = 8,
[String("track,album,artist,playlist")]
All = 16
}
}

View File

@ -1,20 +0,0 @@
using System;
namespace SpotifyAPI.Web.Enums
{
/// <summary>
/// Only one value allowed
/// </summary>
[Flags]
public enum TimeRangeType
{
[String("long_term")]
LongTerm = 1,
[String("medium_term")]
MediumTerm = 2,
[String("short_term")]
ShortTerm = 4
}
}

View File

@ -1,20 +0,0 @@
using System;
namespace SpotifyAPI.Web.Enums
{
[Flags]
public enum TrackType
{
[String("track")]
Track = 1,
[String("episode")]
Episode = 2,
[String("ad")]
Ad = 4,
[String("unknown")]
Unknown = 8
}
}

View File

@ -12,16 +12,23 @@ namespace SpotifyAPI.Web.Http
private readonly IAuthenticator _authenticator; private readonly IAuthenticator _authenticator;
private readonly IJSONSerializer _jsonSerializer; private readonly IJSONSerializer _jsonSerializer;
private readonly IHTTPClient _httpClient; private readonly IHTTPClient _httpClient;
private readonly IRetryHandler _retryHandler;
public APIConnector(Uri baseAddress, IAuthenticator authenticator) : public APIConnector(Uri baseAddress, IAuthenticator authenticator) :
this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient()) this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient(), null)
{ } { }
public APIConnector(Uri baseAddress, IAuthenticator authenticator, IJSONSerializer jsonSerializer, IHTTPClient httpClient) public APIConnector(
Uri baseAddress,
IAuthenticator authenticator,
IJSONSerializer jsonSerializer,
IHTTPClient httpClient,
IRetryHandler retryHandler)
{ {
_baseAddress = baseAddress; _baseAddress = baseAddress;
_authenticator = authenticator; _authenticator = authenticator;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_httpClient = httpClient; _httpClient = httpClient;
_retryHandler = retryHandler;
} }
public Task<T> Delete<T>(Uri uri) public Task<T> Delete<T>(Uri uri)
@ -106,7 +113,7 @@ namespace SpotifyAPI.Web.Http
_httpClient.SetRequestTimeout(timeout); _httpClient.SetRequestTimeout(timeout);
} }
private async Task<T> SendAPIRequest<T>( public async Task<T> SendAPIRequest<T>(
Uri uri, Uri uri,
HttpMethod method, HttpMethod method,
IDictionary<string, string> parameters = null, IDictionary<string, string> parameters = null,
@ -128,6 +135,11 @@ namespace SpotifyAPI.Web.Http
_jsonSerializer.SerializeRequest(request); _jsonSerializer.SerializeRequest(request);
await _authenticator.Apply(request).ConfigureAwait(false); await _authenticator.Apply(request).ConfigureAwait(false);
IResponse response = await _httpClient.DoRequest(request).ConfigureAwait(false); IResponse response = await _httpClient.DoRequest(request).ConfigureAwait(false);
response = await _retryHandler?.HandleRetry(request, response, async (newRequest) =>
{
await _authenticator.Apply(newRequest).ConfigureAwait(false);
return await _httpClient.DoRequest(request).ConfigureAwait(false);
});
ProcessErrors(response); ProcessErrors(response);
IAPIResponse<T> apiResponse = _jsonSerializer.DeserializeResponse<T>(response); IAPIResponse<T> apiResponse = _jsonSerializer.DeserializeResponse<T>(response);

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace SpotifyAPI.Web.Http namespace SpotifyAPI.Web.Http
@ -27,6 +28,8 @@ namespace SpotifyAPI.Web.Http
Task<T> Delete<T>(Uri uri, IDictionary<string, string> parameters); Task<T> Delete<T>(Uri uri, IDictionary<string, string> parameters);
Task<T> Delete<T>(Uri uri, IDictionary<string, string> parameters, object body); Task<T> Delete<T>(Uri uri, IDictionary<string, string> parameters, object body);
Task<T> SendAPIRequest<T>(Uri uri, HttpMethod method, IDictionary<string, string> parameters = null, object body = null);
void SetRequestTimeout(TimeSpan timeout); void SetRequestTimeout(TimeSpan timeout);
} }
} }

View File

@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
namespace SpotifyAPI.Web.Http
{
/// <summary>
/// The Retry Handler will be directly called after the response is retrived and before errors and body are processed.
/// </summary>
public interface IRetryHandler
{
Task<IResponse> HandleRetry(IRequest request, IResponse response, Func<IRequest, Task<IResponse>> retry);
}
}

View File

@ -1,41 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace SpotifyAPI.Web
{
public static class Util
{
public static string GetStringAttribute<T>(this T en, string separator = "") where T : struct, IConvertible
{
Enum e = (Enum) (object) en;
IEnumerable<StringAttribute> attributes =
Enum.GetValues(typeof(T))
.Cast<T>()
.Where(v => e.HasFlag((Enum) (object) v))
.Select(v => typeof(T).GetField(v.ToString(CultureInfo.InvariantCulture)))
.Select(f => f.GetCustomAttributes(typeof(StringAttribute), false) [0])
.Cast<StringAttribute>();
List<string> list = new List<string>();
attributes.ToList().ForEach(element => list.Add(element.Text));
return string.Join(separator, list);
}
public static long ToUnixTimeMillisecondsPoly(this DateTime time)
{
return (long) time.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds;
}
}
public sealed class StringAttribute : Attribute
{
public string Text { get; set; }
public StringAttribute(string text)
{
Text = text;
}
}
}