diff --git a/Source/StrongGrid/Extensions/Internal.cs b/Source/StrongGrid/Extensions/Internal.cs index f6f1a6b4..661c2ba9 100644 --- a/Source/StrongGrid/Extensions/Internal.cs +++ b/Source/StrongGrid/Extensions/Internal.cs @@ -269,10 +269,12 @@ internal static Task> 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(link, propertyName, jsonConverter); + var paginationLinks = PaginationLinksParser.Parse(link); + + return response.Message.Content.AsPaginatedResponseWithLinks(paginationLinks, propertyName, jsonConverter); } /// Asynchronously retrieve the JSON encoded content and convert it to a 'AsPaginatedResponseWithLinks' object. @@ -915,12 +917,12 @@ private static async Task> AsPaginatedResponse(this Http /// 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 pagination links. /// 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) + private static async Task> AsPaginatedResponseWithLinks(this HttpContent httpContent, IEnumerable paginationLinks, string propertyName, JsonConverter jsonConverter = null) { var responseContent = await httpContent.ReadAsStringAsync(null).ConfigureAwait(false); @@ -945,38 +947,12 @@ private static async Task> AsPaginatedResponseWith 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"), + 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 }; diff --git a/Source/StrongGrid/Models/PaginationLink.cs b/Source/StrongGrid/Models/PaginationLink.cs index d7bb7cc3..f1a436c5 100644 --- a/Source/StrongGrid/Models/PaginationLink.cs +++ b/Source/StrongGrid/Models/PaginationLink.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + namespace StrongGrid.Models { /// @@ -6,21 +9,36 @@ namespace StrongGrid.Models public class PaginationLink { /// - /// Gets or sets the uri to the page. + /// Gets or sets the uri. /// - public string Link { get; set; } + public Uri Link { get; set; } /// - /// Gets or sets the value describing the pagination link. + /// Gets or sets the relationship. + /// + public string Relationship { get; set; } + + /// + /// Gets or sets the relationship. /// /// - /// 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???? /// - public string Rel { get; set; } + public string Rev { get; set; } + + /// + /// Gets or sets the title. + /// + public string Title { get; set; } + + /// + /// Gets or sets the anchor. + /// + public Uri Anchor { get; set; } /// - /// Gets or sets the page number. + /// Gets or sets the extensions. /// - public int PageNumber { get; set; } + public IEnumerable> Extensions { get; set; } } } diff --git a/Source/StrongGrid/Utilities/PaginationLinksParser.cs b/Source/StrongGrid/Utilities/PaginationLinksParser.cs new file mode 100644 index 00000000..9137f9a0 --- /dev/null +++ b/Source/StrongGrid/Utilities/PaginationLinksParser.cs @@ -0,0 +1,64 @@ +using StrongGrid.Models; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StrongGrid.Utilities +{ + /// + /// Parser for pagination links. + /// + /// + /// This parser attempts to respect the HTTP 1.1 'Link' rules defined in RFC 2068. + /// + public static class PaginationLinksParser + { + private static readonly string[] _wellKnownLinkParts = new[] { "rel", "rev", "title", "anchor" }; + + /// + /// Parse the content of the link response header. + /// + /// The content of the link header. + /// An array of . + 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(); + 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(); + } + } +}