From 4537223285d8307a091b4309391aa20cef11f8b6 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 --- .../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 | 2 +- 6 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 Source/StrongGrid/Models/PaginatedResponseWithLinks.cs create mode 100644 Source/StrongGrid/Models/PaginationLink.cs diff --git a/Source/StrongGrid.UnitTests/Resources/GlobalSuppressionTests.cs b/Source/StrongGrid.UnitTests/Resources/GlobalSuppressionTests.cs index 9ffc21e0..a3e05055 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 b13c4a15..b1874675 100644 --- a/Source/StrongGrid/Extensions/Internal.cs +++ b/Source/StrongGrid/Extensions/Internal.cs @@ -259,6 +259,37 @@ internal static async Task AsRawJsonDocument(this IRequest request return await response.AsRawJsonDocument(propertyName, throwIfPropertyIsMissing).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. @@ -967,6 +998,77 @@ 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; + } + private static T GetPropertyValue(this JsonElement element, string[] names, T defaultValue, bool throwIfMissing) { JsonElement? property = null; 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 59383bb5..310ae95f 100644 --- a/Source/StrongGrid/Resources/GlobalSuppressions.cs +++ b/Source/StrongGrid/Resources/GlobalSuppressions.cs @@ -53,7 +53,7 @@ internal GlobalSuppressions(Pathoschild.Http.Client.IClient client) /// /// Also note that SendGrid requires that your search term contain at least three characters. /// - public Task GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, string searchTerm = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default) + public Task> GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, string searchTerm = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default) { return _client .GetAsync("suppression/unsubscribes") @@ -64,7 +64,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 9506b1e6..af0e9bdc 100644 --- a/Source/StrongGrid/Resources/IGlobalSuppressions.cs +++ b/Source/StrongGrid/Resources/IGlobalSuppressions.cs @@ -36,7 +36,7 @@ public interface IGlobalSuppressions /// /// Also note that SendGrid requires that your search term contain at least three characters. /// - Task GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, string searchTerm = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default); + Task> GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, string searchTerm = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default); /// /// Check if a recipient address is in the global suppressions group.