Skip to content

Commit

Permalink
Prepare ability to check rate limits (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinTTY committed Aug 18, 2024
1 parent 8c4d580 commit 9423205
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 16 deletions.
4 changes: 2 additions & 2 deletions docs/release-notes/v10_0_0.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public async Task<NordigenApiResponse<List<Institution>, BasicResponse>> GetInst

The `TokenPairUpdated` event is now raised whenever the `JsonWebTokenPair` property is successfully updated. Not only when it was automatically updated but also when done so by the user.

**Full Changelog**: [v9.0.0...v10.0.0](https://github.com/RobinTTY/NordigenApiClient/compare/v9.0.0...v10.0.0)

## Other improvements

A full documentation of the library is now available at: [https://robintty.github.io/NordigenApiClient/](https://robintty.github.io/NordigenApiClient/)

**Full Changelog**: [v9.0.0...v10.0.0](https://github.com/RobinTTY/NordigenApiClient/compare/v9.0.0...v10.0.0)
9 changes: 6 additions & 3 deletions src/RobinTTY.NordigenApiClient/Endpoints/AccountsEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ private async Task<NordigenApiResponse<List<Balance>, AccountsError>> GetBalance
{
var response = await _nordigenClient.MakeRequest<BalanceJsonWrapper, AccountsError>(
$"{NordigenEndpointUrls.AccountsEndpoint}{accountId}/balances/", HttpMethod.Get, cancellationToken);

return new NordigenApiResponse<List<Balance>, AccountsError>(response.StatusCode, response.IsSuccess,
response.Result?.Balances, response.Error);
response.Result?.Balances, response.Error, response.RateLimits);
}

/// <inheritdoc />
Expand All @@ -65,8 +66,9 @@ private async Task<NordigenApiResponse<BankAccountDetails, AccountsError>> GetAc
{
var response = await _nordigenClient.MakeRequest<BankAccountDetailsWrapper, AccountsError>(
$"{NordigenEndpointUrls.AccountsEndpoint}{id}/details/", HttpMethod.Get, cancellationToken);

return new NordigenApiResponse<BankAccountDetails, AccountsError>(response.StatusCode, response.IsSuccess,
response.Result?.Account, response.Error);
response.Result?.Account, response.Error, response.RateLimits);
}

/// <inheritdoc />
Expand Down Expand Up @@ -111,8 +113,9 @@ private async Task<NordigenApiResponse<AccountTransactions, AccountsError>> GetT

var response = await _nordigenClient.MakeRequest<AccountTransactionsWrapper, AccountsError>(
$"{NordigenEndpointUrls.AccountsEndpoint}{id}/transactions/", HttpMethod.Get, cancellationToken, query);

return new NordigenApiResponse<AccountTransactions, AccountsError>(response.StatusCode, response.IsSuccess,
response.Result?.Transactions, response.Error);
response.Result?.Transactions, response.Error, response.RateLimits);
}

#if NET6_0_OR_GREATER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ public async Task<NordigenApiResponse<List<Institution>, BasicResponse>> GetInst
NordigenEndpointUrls.InstitutionsEndpoint, HttpMethod.Get, cancellationToken, query);

return new NordigenApiResponse<List<Institution>, BasicResponse>(response.StatusCode, response.IsSuccess,
response.Result,
response.Error);
response.Result, response.Error, response.RateLimits);
}

/// <inheritdoc />
Expand Down
60 changes: 60 additions & 0 deletions src/RobinTTY.NordigenApiClient/Models/Responses/ApiRateLimits.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using RobinTTY.NordigenApiClient.Endpoints;

namespace RobinTTY.NordigenApiClient.Models.Responses;

/// <summary>
/// The rate limits of the GoCardless API.
/// </summary>
public class ApiRateLimits
{
/// <summary>
/// Indicates the maximum number of allowed requests within the defined time window.
/// </summary>
public int RequestLimit { get; set; }
/// <summary>
/// Indicates the number of remaining requests you can make in the current time window.
/// </summary>
public int RemainingRequests { get; set; }
/// <summary>
/// Indicates the time remaining in the current time window (in seconds).
/// </summary>
public int RemainingSecondsInTimeWindow { get; set; }
/// <summary>
/// Indicates the maximum number of allowed requests to the <see cref="AccountsEndpoint"/>
/// within the defined time window.
/// </summary>
public int MaxAccountRequests { get; set; }
/// <summary>
/// Indicates the number of remaining requests to the <see cref="AccountsEndpoint"/>
/// you can make in the current time window.
/// </summary>
public int RemainingAccountRequests { get; set; }
/// <summary>
/// Indicates the time remaining in the current time window (in seconds) for requests
/// to the <see cref="AccountsEndpoint"/>.
/// </summary>
public int RemainingSecondsInAccountTimeWindow { get; set; }

/// <summary>
/// Creates a new instance of <see cref="ApiRateLimits" />.
/// </summary>
/// <param name="requestLimit">Indicates the maximum number of allowed requests within the defined time window.</param>
/// <param name="remainingRequests">Indicates the number of remaining requests you can make in the current time window.</param>
/// <param name="remainingTimeInTimeWindow">Indicates the time remaining in the current time window (in seconds).</param>
/// <param name="maxAccountRequests">Indicates the maximum number of allowed requests to the <see cref="AccountsEndpoint"/>
/// within the defined time window.</param>
/// <param name="remainingAccountRequests">Indicates the number of remaining requests to the <see cref="AccountsEndpoint"/>
/// you can make in the current time window.</param>
/// <param name="remainingTimeInAccountTimeWindow">Indicates the time remaining in the current time window (in seconds) for requests
/// to the <see cref="AccountsEndpoint"/>.</param>
public ApiRateLimits(int requestLimit, int remainingRequests, int remainingTimeInTimeWindow, int maxAccountRequests,
int remainingAccountRequests, int remainingTimeInAccountTimeWindow)
{
RequestLimit = requestLimit;
RemainingRequests = remainingRequests;
RemainingSecondsInTimeWindow = remainingTimeInTimeWindow;
MaxAccountRequests = maxAccountRequests;
RemainingAccountRequests = remainingAccountRequests;
RemainingSecondsInAccountTimeWindow = remainingTimeInAccountTimeWindow;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public class NordigenApiResponse<TResult, TError> where TResult : class where TE
/// The error returned by the API. Null if the HTTP response was successful.
/// </summary>
public TError? Error { get; }

/// <summary>
/// The rate limits of the GoCardless API.
/// </summary>
public ApiRateLimits RateLimits { get; }

/// <summary>
/// Creates a new instance of <see cref="NordigenApiResponse{TResult, TError}" />.
Expand All @@ -42,12 +47,15 @@ public class NordigenApiResponse<TResult, TError> where TResult : class where TE
/// <param name="isSuccess">Indicates whether the HTTP response was successful.</param>
/// <param name="result">The result returned by the API. Null if the the HTTP response was not successful.</param>
/// <param name="apiError">The error returned by the API. Null if the HTTP response was successful.</param>
public NordigenApiResponse(HttpStatusCode statusCode, bool isSuccess, TResult? result, TError? apiError)
/// <param name="rateLimits">The rate limits of the GoCardless API.</param>
public NordigenApiResponse(HttpStatusCode statusCode, bool isSuccess, TResult? result,
TError? apiError, ApiRateLimits rateLimits)
{
StatusCode = statusCode;
IsSuccess = isSuccess;
Result = result;
Error = apiError;
RateLimits = rateLimits;
}

/// <summary>
Expand All @@ -60,25 +68,25 @@ public NordigenApiResponse(HttpStatusCode statusCode, bool isSuccess, TResult? r
internal static async Task<NordigenApiResponse<TResult, TError>> FromHttpResponse(HttpResponseMessage response,
JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
{
var rateLimits = GetRateLimits(response);
#if NET6_0_OR_GREATER
var responseJson = await response.Content.ReadAsStringAsync(cancellationToken);
#else
var responseJson = await response.Content.ReadAsStringAsync();
#endif

try
{
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<TResult>(options, cancellationToken);
return new NordigenApiResponse<TResult, TError>(response.StatusCode, response.IsSuccessStatusCode,
result, null);
result, null, rateLimits);
}
else
{
var result = await response.Content.ReadFromJsonAsync<TError>(options, cancellationToken);
return new NordigenApiResponse<TResult, TError>(response.StatusCode, response.IsSuccessStatusCode,
null, result);
null, result, rateLimits);
}
}
catch (JsonException ex)
Expand All @@ -88,4 +96,22 @@ internal static async Task<NordigenApiResponse<TResult, TError>> FromHttpRespons
$"The following JSON content caused the problem: {responseJson}", ex);
}
}

private static ApiRateLimits GetRateLimits(HttpResponseMessage response)
{
response.Headers.TryGetValues("HTTP_X_RATELIMIT_LIMIT", out var requestLimitInTimeWindow);
response.Headers.TryGetValues("HTTP_X_RATELIMIT_REMAINING", out var remainingRequestsInTimeWindow);
response.Headers.TryGetValues("HTTP_X_RATELIMIT_RESET", out var remainingTimeInTimeWindow);
response.Headers.TryGetValues("HTTP_X_RATELIMIT_ACCOUNT_SUCCESS_LIMIT", out var maxAccountRequestsInTimeWindow);
response.Headers.TryGetValues("HTTP_X_RATELIMIT_ACCOUNT_SUCCESS_REMAINING", out var remainingAccountRequestsInTimeWindow);
response.Headers.TryGetValues("HTTP_X_RATELIMIT_ACCOUNT_SUCCESS_RESET", out var remainingTimeInAccountTimeWindow);

return new ApiRateLimits(
requestLimitInTimeWindow != null ? int.Parse(requestLimitInTimeWindow.First()) : 0,
remainingRequestsInTimeWindow != null ? int.Parse(remainingRequestsInTimeWindow.First()) : 0,
remainingTimeInTimeWindow != null ? int.Parse(remainingTimeInTimeWindow.First()) : 0,
maxAccountRequestsInTimeWindow != null ? int.Parse(maxAccountRequestsInTimeWindow.First()) : 0,
remainingAccountRequestsInTimeWindow != null ? int.Parse(remainingAccountRequestsInTimeWindow.First()) : 0,
remainingTimeInAccountTimeWindow != null ? int.Parse(remainingTimeInAccountTimeWindow.First()) : 0);
}
}
9 changes: 5 additions & 4 deletions src/RobinTTY.NordigenApiClient/NordigenClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ internal async Task<NordigenApiResponse<TResponse, TError>> MakeRequest<TRespons
JsonWebTokenPair = tokenResponse.Result;
else
return new NordigenApiResponse<TResponse, TError>(tokenResponse.StatusCode, tokenResponse.IsSuccess,
null, new TError {Summary = tokenResponse.Error.Summary, Detail = tokenResponse.Error.Detail});
null, new TError {Summary = tokenResponse.Error.Summary, Detail = tokenResponse.Error.Detail},
tokenResponse.RateLimits);
}
finally
{
Expand Down Expand Up @@ -180,13 +181,13 @@ private async Task<NordigenApiResponse<JsonWebTokenPair, BasicResponse>> TryGetV
? new JsonWebTokenPair(response.Result.AccessToken, JsonWebTokenPair.RefreshToken,
response.Result!.AccessExpires, JsonWebTokenPair.RefreshExpires)
: null;

return new NordigenApiResponse<JsonWebTokenPair, BasicResponse>(response.StatusCode, response.IsSuccess,
token, response.Error);
token, response.Error, response.RateLimits);
}

// Token pair is still valid and can be returned - wrap in NordigenApiResponse
return new NordigenApiResponse<JsonWebTokenPair, BasicResponse>(HttpStatusCode.OK, true, JsonWebTokenPair,
null);
null, new ApiRateLimits(-1, -1, -1, -1, 0, -1));
}
}
2 changes: 1 addition & 1 deletion src/RobinTTY.NordigenApiClient/release-notes.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
This version improves many aspects of the library. Since this version contains breaking changes please check the release notes before updating.
For the full release notes please see: https://github.com/RobinTTY/NordigenApiClient/releases/tag/v9.0.0
For the full release notes please see: https://github.com/RobinTTY/NordigenApiClient/releases/tag/v10.0.0

0 comments on commit 9423205

Please sign in to comment.