Pagination link parser

use "peg"

primitive ExtractPaginationLinks
  fun apply(link: String): (PaginationLinks | ParseError) =>
    let source = Source.from_string(link)
    match recover val _PaginationLinkParser().parse(source) end
    | (_, let r: ASTChild) =>
      return _build(r)
    | (let offset: USize, let r: Parser val) =>
      ParseError(recover val SyntaxError(source, offset, r) end)
    else
      Unreachable()
      PaginationLinks
    end

  fun _build(p: ASTChild): PaginationLinks =>
    var prev: (String | None) = None
    var next: (String | None) = None
    var first: (String | None) = None
    var last: (String | None) = None

    match p
    | let top: AST =>
      for child in top.children.values() do
        if child.label() is _TPair then
          match child
          | let ast: AST =>
            var rel = ""
            var url = ""
            for child' in ast.children.values() do
              match child'
              | let token: Token =>
                if token.label() is _TRel then
                  rel = token.string()
                elseif token.label() is _TURL then
                  url = token.string()
                end
              end

              if (rel != "") and (url != "") then
                match rel
                | "prev" => prev = url
                | "next" => next = url
                | "first" => first = url
                | "last" => last = url
                end

                rel = ""
                url = ""
              end
            end
          end
        end
      end
    end

    PaginationLinks(prev, next, first, last)

  fun _process_pair(token: Token): (String, String) =>
    (token.label().text(), token.string())

class val PaginationLinks
  let prev: (String | None)
  let next: (String | None)
  let first: (String | None)
  let last: (String | None)

  new val create(prev': (String | None) = None,
    next': (String | None) = None,
    first': (String | None) = None,
    last': (String | None) = None)
  =>
    prev = prev'
    next = next'
    first = first'
    last =  last'

// TODO: this "as json" is really only relevant for GitHub REST API
// if we kick this parser out as own library, we need to either expose
// the peg library (and add a simple string() error converter) or create
// our own wrapper
class val ParseError
  let message: String

  new val create(e: PegError val) =>
    message = recover
      let m: String ref = String
      let ba = PegFormatError.json(e)
      for b in ba.values() do
        m.append(b)
      end
      m
    end

primitive _PaginationLinkParser
  fun apply(): Parser val =>
    recover
      let digit = R('0', '9')
      let hex = digit / R('a', 'f') / R('A', 'F')
      let url_char = (L("\\u") * hex * hex * hex * hex) / (not L(">") * R(' '))

      let url = -L("<") * (url_char.many()).term(_TURL) * -L(">;")

      let prev = L("prev")
      let next = L("next")
      let first = L("first")
      let last = L("last")

      let rel =
        -L("rel=\"") * (prev / next / first / last).term(_TRel) * -L("\"")
      let link_and_rel = (url * -L(" ") * rel).node(_TPair)
      (link_and_rel * -L(", ").opt()).many()
    end

primitive _TPair is Label fun text(): String => "Pair"
primitive _TRel is Label fun text(): String => "Rel"
primitive _TURL is Label fun text(): String => "Link"