Skip to content

Commit

Permalink
(GH-361) Add a way to return pagination information that SendGrid ret…
Browse files Browse the repository at this point in the history
…urns in a Link' response header
  • Loading branch information
Jericho committed Apr 12, 2024
1 parent 6a70217 commit 4537223
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task GetAll()
mockHttp.VerifyNoOutstandingExpectation();
mockHttp.VerifyNoOutstandingRequest();
result.ShouldNotBeNull();
result.Length.ShouldBe(3);
result.Records.Length.ShouldBe(3);
}

[Fact]
Expand Down
102 changes: 102 additions & 0 deletions Source/StrongGrid/Extensions/Internal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,37 @@ internal static async Task<JsonDocument> AsRawJsonDocument(this IRequest request
return await response.AsRawJsonDocument(propertyName, throwIfPropertyIsMissing).ConfigureAwait(false);
}

/// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'AsPaginatedResponseWithLinks' object.</summary>
/// <typeparam name="T">The response model to deserialize into.</typeparam>
/// <param name="response">The response.</param>
/// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
/// <param name="jsonConverter">Converter that will be used during deserialization.</param>
/// <returns>Returns the paginated response.</returns>
/// <exception cref="SendGridException">An error occurred processing the response.</exception>
internal static Task<PaginatedResponseWithLinks<T>> AsPaginatedResponseWithLinks<T>(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<T>(link, propertyName, jsonConverter);
}

/// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'AsPaginatedResponseWithLinks' object.</summary>
/// <typeparam name="T">The response model to deserialize into.</typeparam>
/// <param name="request">The request.</param>
/// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
/// <param name="jsonConverter">Converter that will be used during deserialization.</param>
/// <returns>Returns the paginated response.</returns>
/// <exception cref="SendGridException">An error occurred processing the response.</exception>
internal static async Task<PaginatedResponseWithLinks<T>> AsPaginatedResponseWithLinks<T>(this IRequest request, string propertyName = null, JsonConverter jsonConverter = null)
{
var response = await request.AsResponse().ConfigureAwait(false);
return await response.AsPaginatedResponseWithLinks<T>(propertyName, jsonConverter).ConfigureAwait(false);
}

/// <summary>Set the body content of the HTTP request.</summary>
/// <typeparam name="T">The type of object to serialize into a JSON string.</typeparam>
/// <param name="request">The request.</param>
Expand Down Expand Up @@ -967,6 +998,77 @@ private static async Task<PaginatedResponse<T>> AsPaginatedResponse<T>(this Http
return result;
}

/// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponseWithLinks' object.</summary>
/// <typeparam name="T">The response model to deserialize into.</typeparam>
/// <param name="httpContent">The content.</param>
/// <param name="linkHeaderValue">The content of the 'Link' header.</param>
/// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
/// <param name="jsonConverter">Converter that will be used during deserialization.</param>
/// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
/// <exception cref="SendGridException">An error occurred processing the response.</exception>
private static async Task<PaginatedResponseWithLinks<T>> AsPaginatedResponseWithLinks<T>(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<T[]>(serializer) ?? Array.Empty<T>();
}
else
{
records = JArray.Parse(responseContent).ToObject<T[]>(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<T>()
{
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<T>(this JsonElement element, string[] names, T defaultValue, bool throwIfMissing)
{
JsonElement? property = null;
Expand Down
34 changes: 34 additions & 0 deletions Source/StrongGrid/Models/PaginatedResponseWithLinks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace StrongGrid.Models
{
/// <summary>
/// Pagination Object.
/// </summary>
/// <typeparam name="T">The type of records.</typeparam>
public class PaginatedResponseWithLinks<T>
{
/// <summary>
/// Gets or sets the information about the first page.
/// </summary>
public PaginationLink First { get; set; }

/// <summary>
/// Gets or sets the information about the previous page.
/// </summary>
public PaginationLink Previous { get; set; }

/// <summary>
/// Gets or sets the information about the next page.
/// </summary>
public PaginationLink Next { get; set; }

/// <summary>
/// Gets or sets the information about the last page.
/// </summary>
public PaginationLink Last { get; set; }

/// <summary>
/// Gets or sets the records.
/// </summary>
public T[] Records { get; set; }
}
}
11 changes: 11 additions & 0 deletions Source/StrongGrid/Models/PaginationLink.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
4 changes: 2 additions & 2 deletions Source/StrongGrid/Resources/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </remarks>
public Task<GlobalSuppression[]> GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, string searchTerm = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default)
public Task<PaginatedResponseWithLinks<GlobalSuppression>> 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")
Expand All @@ -64,7 +64,7 @@ public Task<GlobalSuppression[]> GetAllAsync(DateTime? startDate = null, DateTim
.WithArgument("limit", limit)
.WithArgument("offset", offset)
.WithCancellationToken(cancellationToken)
.AsObject<GlobalSuppression[]>();
.AsPaginatedResponseWithLinks<GlobalSuppression>();
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Source/StrongGrid/Resources/IGlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public interface IGlobalSuppressions
///
/// Also note that SendGrid requires that your search term contain at least three characters.
/// </remarks>
Task<GlobalSuppression[]> GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, string searchTerm = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default);
Task<PaginatedResponseWithLinks<GlobalSuppression>> GetAllAsync(DateTime? startDate = null, DateTime? endDate = null, string searchTerm = null, int limit = 50, int offset = 0, string onBehalfOf = null, CancellationToken cancellationToken = default);

/// <summary>
/// Check if a recipient address is in the global suppressions group.
Expand Down

0 comments on commit 4537223

Please sign in to comment.