Skip to content

Commit

Permalink
(GH-361) More robust 'Link' parser
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed Aug 4, 2024
1 parent 15dba0f commit 29ac326
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 41 deletions.
44 changes: 10 additions & 34 deletions Source/StrongGrid/Extensions/Internal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,12 @@ internal static Task<PaginatedResponseWithLinks<T>> AsPaginatedResponseWithLinks
var link = response.Message.Headers.GetValue("Link");
if (string.IsNullOrEmpty(link))
{
throw new Exception("The 'Link' header is missing form the response");
throw new Exception("The 'Link' header is missing from the response");
}

return response.Message.Content.AsPaginatedResponseWithLinks<T>(link, propertyName, jsonConverter);
var paginationLinks = PaginationLinksParser.Parse(link);

return response.Message.Content.AsPaginatedResponseWithLinks<T>(paginationLinks, propertyName, jsonConverter);
}

/// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'AsPaginatedResponseWithLinks' object.</summary>
Expand Down Expand Up @@ -1021,12 +1023,12 @@ private static async Task<PaginatedResponse<T>> AsPaginatedResponse<T>(this Http
/// <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="paginationLinks">The pagination links.</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)
private static async Task<PaginatedResponseWithLinks<T>> AsPaginatedResponseWithLinks<T>(this HttpContent httpContent, IEnumerable<PaginationLink> paginationLinks, string propertyName, JsonConverter jsonConverter = null)
{
var responseContent = await httpContent.ReadAsStringAsync(null).ConfigureAwait(false);

Expand All @@ -1051,38 +1053,12 @@ private static async Task<PaginatedResponseWithLinks<T>> AsPaginatedResponseWith
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"),
First = paginationLinks.SingleOrDefault(l => l.Relationship == "first"),
Previous = paginationLinks.SingleOrDefault(l => l.Relationship == "prev"),
Next = paginationLinks.SingleOrDefault(l => l.Relationship == "next"),
Last = paginationLinks.SingleOrDefault(l => l.Relationship == "last"),
Records = records
};

Expand Down
32 changes: 25 additions & 7 deletions Source/StrongGrid/Models/PaginationLink.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;

namespace StrongGrid.Models
{
/// <summary>
Expand All @@ -6,21 +9,36 @@ namespace StrongGrid.Models
public class PaginationLink
{
/// <summary>
/// Gets or sets the uri to the page.
/// Gets or sets the uri.
/// </summary>
public string Link { get; set; }
public Uri Link { get; set; }

/// <summary>
/// Gets or sets the value describing the pagination link.
/// Gets or sets the relationship.
/// </summary>
public string Relationship { get; set; }

/// <summary>
/// Gets or sets the relationship.
/// </summary>
/// <remarks>
/// There are 4 possible values: first, prev, next and last.
/// 'Rev' is obviously short for something but I can't figure out what it stands for????
/// </remarks>
public string Rel { get; set; }
public string Rev { get; set; }

/// <summary>
/// Gets or sets the title.
/// </summary>
public string Title { get; set; }

/// <summary>
/// Gets or sets the anchor.
/// </summary>
public Uri Anchor { get; set; }

/// <summary>
/// Gets or sets the page number.
/// Gets or sets the extensions.
/// </summary>
public int PageNumber { get; set; }
public IEnumerable<KeyValuePair<string, string>> Extensions { get; set; }
}
}
64 changes: 64 additions & 0 deletions Source/StrongGrid/Utilities/PaginationLinksParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using StrongGrid.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace StrongGrid.Utilities
{
/// <summary>
/// Parser for pagination links.
/// </summary>
/// <remarks>
/// This parser attempts to respect the HTTP 1.1 'Link' rules defined in <see cref="https://tools.ietf.org/html/rfc2068#section-19.6.2.4">RFC 2068</see>.
/// </remarks>
public static class PaginationLinksParser
{
private static readonly string[] _wellKnownLinkParts = new[] { "rel", "rev", "title", "anchor" };

/// <summary>
/// Parse the content of the link response header.
/// </summary>
/// <param name="linkHeaderContent">The content of the link header.</param>
/// <returns>An array of <see cref="PaginationLink"/>.</returns>
public static PaginationLink[] Parse(string linkHeaderContent)
{
var paginationLinks = linkHeaderContent
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(link => link.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
.Select(linkParts =>
{
var dict = new Dictionary<string, string>();
foreach (var linkPart in linkParts.Skip(1))
{
var parts = linkPart.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
var partName = parts[0].Trim();
var partValue = parts[1].Trim(new[] { ' ', '"' });
dict.Add(partName, partValue);
}
var link = linkParts[0].Trim().TrimStart(new[] { '<' }).TrimEnd(new[] { '>' }).Trim();
dict.TryGetValue("rel", out string rel);
dict.TryGetValue("rev", out string rev);
dict.TryGetValue("title", out string title);
dict.TryGetValue("anchor", out string anchor);
var extensions = dict
.Where(item => !_wellKnownLinkParts.Contains(item.Key))
.ToArray();
return new PaginationLink()
{
Link = !string.IsNullOrEmpty(link) ? new Uri(link) : null,
Relationship = rel,
Rev = rev,
Title = title,
Anchor = !string.IsNullOrEmpty(anchor) ? new Uri(anchor) : null,
Extensions = extensions
};
});

return paginationLinks.ToArray();
}
}
}

0 comments on commit 29ac326

Please sign in to comment.