From f41e029b97bab647aea7a03b09bd74ce5ba28ebf Mon Sep 17 00:00:00 2001 From: Jericho Date: Mon, 18 Jan 2021 13:43:34 -0500 Subject: [PATCH] (GH-361) Add a way to return pagination information that SendGrid returns in a Link' response header --- .../Tests/GlobalSuppressions.cs | 4 + .../Resources/GlobalSuppressionTests.cs | 2 +- Source/StrongGrid/Extensions/Internal.cs | 102 ++++++++++++++++++ .../Models/PaginatedResponseWithLinks.cs | 34 ++++++ Source/StrongGrid/Models/PaginationLink.cs | 11 ++ .../Resources/GlobalSuppressions.cs | 4 +- .../Resources/IGlobalSuppressions.cs | 4 +- 7 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 Source/StrongGrid/Models/PaginatedResponseWithLinks.cs create mode 100644 Source/StrongGrid/Models/PaginationLink.cs diff --git a/Source/StrongGrid.IntegrationTests/Tests/GlobalSuppressions.cs b/Source/StrongGrid.IntegrationTests/Tests/GlobalSuppressions.cs index 0ba9f1b4..084b1a1a 100644 --- a/Source/StrongGrid.IntegrationTests/Tests/GlobalSuppressions.cs +++ b/Source/StrongGrid.IntegrationTests/Tests/GlobalSuppressions.cs @@ -23,6 +23,10 @@ public async Task RunAsync(IBaseClient client, TextWriter log, CancellationToken await log.WriteLineAsync($"Is {emails[0]} unsubscribed (should be true): {isUnsubscribed0}").ConfigureAwait(false); await log.WriteLineAsync($"Is {emails[1]} unsubscribed (should be true): {isUnsubscribed1}").ConfigureAwait(false); + // GET ALL ADDRESSES ON THE SUPPRESSION LIST + var paginatedResult = await client.GlobalSuppressions.GetAllAsync(null, null, 1, 0, null, CancellationToken.None).ConfigureAwait(false); + await log.WriteLineAsync($"There are {paginatedResult.Last.PageNumber} addresses on the global suppression list").ConfigureAwait(false); + // DELETE EMAILS FROM THE GLOBAL SUPPRESSION GROUP await client.GlobalSuppressions.RemoveAsync(emails[0], null, cancellationToken).ConfigureAwait(false); await log.WriteLineAsync($"{emails[0]} has been removed from the global suppression list").ConfigureAwait(false); diff --git a/Source/StrongGrid.UnitTests/Resources/GlobalSuppressionTests.cs b/Source/StrongGrid.UnitTests/Resources/GlobalSuppressionTests.cs index 0effe49a..8f35b20f 100644 --- a/Source/StrongGrid.UnitTests/Resources/GlobalSuppressionTests.cs +++ b/Source/StrongGrid.UnitTests/Resources/GlobalSuppressionTests.cs @@ -48,7 +48,7 @@ public async Task GetAll() mockHttp.VerifyNoOutstandingExpectation(); mockHttp.VerifyNoOutstandingRequest(); result.ShouldNotBeNull(); - result.Length.ShouldBe(3); + result.Records.Length.ShouldBe(3); } [Fact] diff --git a/Source/StrongGrid/Extensions/Internal.cs b/Source/StrongGrid/Extensions/Internal.cs index 4d027ec0..f6f1a6b4 100644 --- a/Source/StrongGrid/Extensions/Internal.cs +++ b/Source/StrongGrid/Extensions/Internal.cs @@ -257,6 +257,37 @@ internal static async Task> AsPaginatedResponse(this IRe return await response.AsPaginatedResponse(propertyName, jsonConverter).ConfigureAwait(false); } + /// Asynchronously retrieve the JSON encoded content and convert it to a 'AsPaginatedResponseWithLinks' object. + /// The response model to deserialize into. + /// The response. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the paginated response. + /// An error occurred processing the response. + internal static Task> AsPaginatedResponseWithLinks(this IResponse response, string propertyName = null, JsonConverter jsonConverter = null) + { + var link = response.Message.Headers.GetValue("Link"); + if (string.IsNullOrEmpty(link)) + { + throw new Exception("The 'Link' header is missing form the response"); + } + + return response.Message.Content.AsPaginatedResponseWithLinks(link, propertyName, jsonConverter); + } + + /// Asynchronously retrieve the JSON encoded content and convert it to a 'AsPaginatedResponseWithLinks' object. + /// The response model to deserialize into. + /// The request. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the paginated response. + /// An error occurred processing the response. + internal static async Task> AsPaginatedResponseWithLinks(this IRequest request, string propertyName = null, JsonConverter jsonConverter = null) + { + var response = await request.AsResponse().ConfigureAwait(false); + return await response.AsPaginatedResponseWithLinks(propertyName, jsonConverter).ConfigureAwait(false); + } + /// Set the body content of the HTTP request. /// The type of object to serialize into a JSON string. /// The request. @@ -880,5 +911,76 @@ private static async Task> AsPaginatedResponse(this Http return result; } + + /// Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponseWithLinks' object. + /// The response model to deserialize into. + /// The content. + /// The content of the 'Link' header. + /// The name of the JSON property (or null if not applicable) where the desired data is stored. + /// Converter that will be used during deserialization. + /// Returns the response body, or null if the response has no body. + /// An error occurred processing the response. + private static async Task> AsPaginatedResponseWithLinks(this HttpContent httpContent, string linkHeaderValue, string propertyName, JsonConverter jsonConverter = null) + { + var responseContent = await httpContent.ReadAsStringAsync(null).ConfigureAwait(false); + + var serializer = new JsonSerializer(); + if (jsonConverter != null) serializer.Converters.Add(jsonConverter); + + T[] records; + + if (!string.IsNullOrEmpty(propertyName)) + { + var jObject = JObject.Parse(responseContent); + var jProperty = jObject.Property(propertyName); + if (jProperty == null) + { + throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName)); + } + + records = jProperty.Value?.ToObject(serializer) ?? Array.Empty(); + } + else + { + records = JArray.Parse(responseContent).ToObject(serializer); + } + + var links = linkHeaderValue + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(link => link.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + .Select(linkParts => + { + var link = linkParts[0] + .Trim() + .TrimStart(new[] { '<' }) + .TrimEnd(new[] { '>' }); + + var rel = linkParts[1] + .Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries)[1] + .Trim(new[] { ' ', '"' }); + + var pageNum = linkParts[2] + .Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries)[1] + .Trim(new[] { ' ', '"' }); + + return new PaginationLink() + { + Link = link, + Rel = rel, + PageNumber = int.Parse(pageNum) + }; + }); + + var result = new PaginatedResponseWithLinks() + { + First = links.Single(l => l.Rel == "first"), + Previous = links.Single(l => l.Rel == "prev"), + Next = links.Single(l => l.Rel == "next"), + Last = links.Single(l => l.Rel == "last"), + Records = records + }; + + return result; + } } } diff --git a/Source/StrongGrid/Models/PaginatedResponseWithLinks.cs b/Source/StrongGrid/Models/PaginatedResponseWithLinks.cs new file mode 100644 index 00000000..2ebc698e --- /dev/null +++ b/Source/StrongGrid/Models/PaginatedResponseWithLinks.cs @@ -0,0 +1,34 @@ +namespace StrongGrid.Models +{ + /// + /// Pagination Object. + /// + /// The type of records. + public class PaginatedResponseWithLinks + { + /// + /// Gets or sets the information about the first page. + /// + public PaginationLink First { get; set; } + + /// + /// Gets or sets the information about the previous page. + /// + public PaginationLink Previous { get; set; } + + /// + /// Gets or sets the information about the next page. + /// + public PaginationLink Next { get; set; } + + /// + /// Gets or sets the information about the last page. + /// + public PaginationLink Last { get; set; } + + /// + /// Gets or sets the records. + /// + public T[] Records { get; set; } + } +} diff --git a/Source/StrongGrid/Models/PaginationLink.cs b/Source/StrongGrid/Models/PaginationLink.cs new file mode 100644 index 00000000..1efa5259 --- /dev/null +++ b/Source/StrongGrid/Models/PaginationLink.cs @@ -0,0 +1,11 @@ +namespace StrongGrid.Models +{ + public class PaginationLink + { + public string Link { get; set; } + + public string Rel { get; set; } + + public int PageNumber { get; set; } + } +} diff --git a/Source/StrongGrid/Resources/GlobalSuppressions.cs b/Source/StrongGrid/Resources/GlobalSuppressions.cs index 817e3185..b74ca514 100644 --- a/Source/StrongGrid/Resources/GlobalSuppressions.cs +++ b/Source/StrongGrid/Resources/GlobalSuppressions.cs @@ -42,7 +42,7 @@ internal GlobalSuppressions(Pathoschild.Http.Client.IClient client) /// /// An array of . /// - public Task GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default) + public Task> GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default) { return _client .GetAsync("suppression/unsubscribes") @@ -52,7 +52,7 @@ public Task GetAllAsync(DateTime? startDate = null, DateTim .WithArgument("limit", limit) .WithArgument("offset", offset) .WithCancellationToken(cancellationToken) - .AsObject(); + .AsPaginatedResponseWithLinks(); } /// diff --git a/Source/StrongGrid/Resources/IGlobalSuppressions.cs b/Source/StrongGrid/Resources/IGlobalSuppressions.cs index 1e058e24..eb6e0b29 100644 --- a/Source/StrongGrid/Resources/IGlobalSuppressions.cs +++ b/Source/StrongGrid/Resources/IGlobalSuppressions.cs @@ -1,4 +1,4 @@ -using StrongGrid.Models; +using StrongGrid.Models; using System; using System.Collections.Generic; using System.Threading; @@ -26,7 +26,7 @@ public interface IGlobalSuppressions /// /// An array of . /// - Task GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default); + Task> GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default); /// /// Check if a recipient address is in the global suppressions group.