summary refs log tree commit diff stats
path: root/tests/deps/jester-#head
diff options
context:
space:
mode:
Diffstat (limited to 'tests/deps/jester-#head')
-rw-r--r--tests/deps/jester-#head/jester.nim1350
-rw-r--r--tests/deps/jester-#head/jester.nimble22
-rw-r--r--tests/deps/jester-#head/jester/patterns.nim141
-rw-r--r--tests/deps/jester-#head/jester/private/errorpages.nim19
-rw-r--r--tests/deps/jester-#head/jester/private/utils.nim195
-rw-r--r--tests/deps/jester-#head/jester/request.nim184
6 files changed, 1911 insertions, 0 deletions
diff --git a/tests/deps/jester-#head/jester.nim b/tests/deps/jester-#head/jester.nim
new file mode 100644
index 000000000..013f0d16d
--- /dev/null
+++ b/tests/deps/jester-#head/jester.nim
@@ -0,0 +1,1350 @@
+# Copyright (C) 2015 Dominik Picheta
+# MIT License - Look at license.txt for details.
+import net, strtabs, re, tables, parseutils, os, strutils, uri,
+       times, mimetypes, asyncnet, asyncdispatch, macros, md5,
+       logging, httpcore, asyncfile, macrocache, json, options,
+       strformat
+
+import jester/private/[errorpages, utils]
+import jester/[request, patterns]
+
+from cgi import decodeData, decodeUrl, CgiError
+
+export request
+export strtabs
+export tables
+export httpcore
+export MultiData
+export HttpMethod
+export asyncdispatch
+
+export SameSite
+
+when useHttpBeast:
+  import httpbeast except Settings, Request
+  import options
+else:
+  import asynchttpserver except Request
+
+type
+  MatchProc* = proc (request: Request): Future[ResponseData] {.gcsafe, closure.}
+  MatchProcSync* = proc (request: Request): ResponseData{.gcsafe, closure.}
+
+  Matcher = object
+    case async: bool
+    of false:
+      syncProc: MatchProcSync
+    of true:
+      asyncProc: MatchProc
+
+  ErrorProc* = proc (
+    request: Request, error: RouteError
+  ): Future[ResponseData] {.gcsafe, closure.}
+
+  Jester* = object
+    when not useHttpBeast:
+      httpServer*: AsyncHttpServer
+    settings: Settings
+    matchers: seq[Matcher]
+    errorHandlers: seq[ErrorProc]
+
+  MatchType* = enum
+    MRegex, MSpecial, MStatic
+
+  RawHeaders* = seq[tuple[key, val: string]]
+  ResponseData* = tuple[
+    action: CallbackAction,
+    code: HttpCode,
+    headers: Option[RawHeaders],
+    content: string,
+    matched: bool
+  ]
+
+  CallbackAction* = enum
+    TCActionNothing, TCActionSend, TCActionRaw, TCActionPass
+
+  RouteErrorKind* = enum
+    RouteException, RouteCode
+  RouteError* = object
+    case kind*: RouteErrorKind
+    of RouteException:
+      exc: ref Exception
+    of RouteCode:
+      data: ResponseData
+
+const jesterVer = "0.4.0"
+
+proc toStr(headers: Option[RawHeaders]): string =
+  return $newHttpHeaders(headers.get(@({:})))
+
+proc createHeaders(headers: RawHeaders): string =
+  result = ""
+  if headers != nil:
+    for header in headers:
+      let (key, value) = header
+      result.add(key & ": " & value & "\c\L")
+
+    result = result[0 .. ^3] # Strip trailing \c\L
+
+proc createResponse(status: HttpCode, headers: RawHeaders): string =
+  return "HTTP/1.1 " & $status & "\c\L" & createHeaders(headers) & "\c\L\c\L"
+
+proc unsafeSend(request: Request, content: string) =
+  when useHttpBeast:
+    request.getNativeReq.unsafeSend(content)
+  else:
+    # TODO: This may cause issues if we send too fast.
+    asyncCheck request.getNativeReq.client.send(content)
+
+proc send(
+  request: Request, code: HttpCode, headers: Option[RawHeaders], body: string
+) =
+  when useHttpBeast:
+    let h =
+      if headers.isNone: ""
+      else: headers.get().createHeaders
+    request.getNativeReq.send(code, body, h)
+  else:
+    # TODO: This may cause issues if we send too fast.
+    asyncCheck request.getNativeReq.respond(
+      code, body, newHttpHeaders(headers.get(@({:})))
+    )
+
+proc statusContent(request: Request, status: HttpCode, content: string,
+                   headers: Option[RawHeaders]) =
+  try:
+    send(request, status, headers, content)
+    when not defined(release):
+      logging.debug("  $1 $2" % [$status, toStr(headers)])
+  except:
+    logging.error("Could not send response: $1" % osErrorMsg(osLastError()))
+
+# TODO: Add support for proper Future Streams instead of this weird raw mode.
+template enableRawMode* =
+  # TODO: Use the effect system to make this implicit?
+  result.action = TCActionRaw
+
+proc send*(request: Request, content: string) =
+  ## Sends ``content`` immediately to the client socket.
+  ##
+  ## Routes using this procedure must enable raw mode.
+  unsafeSend(request, content)
+
+proc sendHeaders*(request: Request, status: HttpCode,
+                  headers: RawHeaders) =
+  ## Sends ``status`` and ``headers`` to the client socket immediately.
+  ## The user is then able to send the content immediately to the client on
+  ## the fly through the use of ``response.client``.
+  let headerData = createResponse(status, headers)
+  try:
+    request.send(headerData)
+    logging.debug("  $1 $2" % [$status, $headers])
+  except:
+    logging.error("Could not send response: $1" % [osErrorMsg(osLastError())])
+
+proc sendHeaders*(request: Request, status: HttpCode) =
+  ## Sends ``status`` and ``Content-Type: text/html`` as the headers to the
+  ## client socket immediately.
+  let headers = @({"Content-Type": "text/html;charset=utf-8"})
+  request.sendHeaders(status, headers)
+
+proc sendHeaders*(request: Request) =
+  ## Sends ``Http200`` and ``Content-Type: text/html`` as the headers to the
+  ## client socket immediately.
+  request.sendHeaders(Http200)
+
+proc send*(request: Request, status: HttpCode, headers: RawHeaders,
+           content: string) =
+  ## Sends out a HTTP response comprising of the ``status``, ``headers`` and
+  ## ``content`` specified.
+  var headers = headers & @({"Content-Length": $content.len})
+  request.sendHeaders(status, headers)
+  request.send(content)
+
+# TODO: Cannot capture 'paths: varargs[string]' here.
+proc sendStaticIfExists(
+  req: Request, paths: seq[string]
+): Future[HttpCode] {.async.} =
+  result = Http200
+  for p in paths:
+    if existsFile(p):
+
+      var fp = getFilePermissions(p)
+      if not fp.contains(fpOthersRead):
+        return Http403
+
+      let fileSize = getFileSize(p)
+      let mimetype = req.settings.mimes.getMimetype(p.splitFile.ext[1 .. ^1])
+      if fileSize < 10_000_000: # 10 mb
+        var file = readFile(p)
+
+        var hashed = getMD5(file)
+
+        # If the user has a cached version of this file and it matches our
+        # version, let them use it
+        if req.headers.hasKey("If-None-Match") and req.headers["If-None-Match"] == hashed:
+          req.statusContent(Http304, "", none[RawHeaders]())
+        else:
+          req.statusContent(Http200, file, some(@({
+                              "Content-Type": mimetype,
+                              "ETag": hashed
+                            })))
+      else:
+        let headers = @({
+          "Content-Type": mimetype,
+          "Content-Length": $fileSize
+        })
+        req.statusContent(Http200, "", some(headers))
+
+        var fileStream = newFutureStream[string]("sendStaticIfExists")
+        var file = openAsync(p, fmRead)
+        # Let `readToStream` write file data into fileStream in the
+        # background.
+        asyncCheck file.readToStream(fileStream)
+        # The `writeFromStream` proc will complete once all the data in the
+        # `bodyStream` has been written to the file.
+        while true:
+          let (hasValue, value) = await fileStream.read()
+          if hasValue:
+            req.unsafeSend(value)
+          else:
+            break
+        file.close()
+
+      return
+
+  # If we get to here then no match could be found.
+  return Http404
+
+proc defaultErrorFilter(error: RouteError): ResponseData =
+  case error.kind
+  of RouteException:
+    let e = error.exc
+    let traceback = getStackTrace(e)
+    var errorMsg = e.msg
+    if errorMsg.isNil: errorMsg = "(nil)"
+
+    let error = traceback & errorMsg
+    logging.error(error)
+    result.headers = some(@({
+      "Content-Type": "text/html;charset=utf-8"
+    }))
+    result.content = routeException(
+      error.replace("\n", "<br/>\n"),
+      jesterVer
+    )
+    result.code = Http502
+    result.matched = true
+    result.action = TCActionSend
+  of RouteCode:
+    result.headers = some(@({
+      "Content-Type": "text/html;charset=utf-8"
+    }))
+    result.content = error(
+      $error.data.code,
+      jesterVer
+    )
+    result.code = error.data.code
+    result.matched = true
+    result.action = TCActionSend
+
+proc initRouteError(exc: ref Exception): RouteError =
+  RouteError(
+    kind: RouteException,
+    exc: exc
+  )
+
+proc initRouteError(data: ResponseData): RouteError =
+  RouteError(
+    kind: RouteCode,
+    data: data
+  )
+
+proc dispatchError(
+  jes: Jester,
+  request: Request,
+  error: RouteError
+): Future[ResponseData] {.async.} =
+  for errorProc in jes.errorHandlers:
+    let data = await errorProc(request, error)
+    if data.matched:
+      return data
+
+  return defaultErrorFilter(error)
+
+proc dispatch(
+  self: Jester,
+  req: Request
+): Future[ResponseData] {.async.} =
+  for matcher in self.matchers:
+    if matcher.async:
+      let data = await matcher.asyncProc(req)
+      if data.matched:
+        return data
+    else:
+      let data = matcher.syncProc(req)
+      if data.matched:
+        return data
+
+proc handleFileRequest(
+  jes: Jester, req: Request
+): Future[ResponseData] {.async.} =
+  # Find static file.
+  # TODO: Caching.
+  let path = normalizedPath(
+    jes.settings.staticDir / cgi.decodeUrl(req.pathInfo)
+  )
+
+  # Verify that this isn't outside our static` dir.
+  var status = Http400
+  if path.splitFile.dir.startsWith(jes.settings.staticDir):
+    if existsDir(path):
+      status = await sendStaticIfExists(
+        req,
+        @[path / "index.html", path / "index.htm"]
+      )
+    else:
+      status = await sendStaticIfExists(req, @[path])
+
+    # Http200 means that the data was sent so there is nothing else to do.
+    if status == Http200:
+      result[0] = TCActionRaw
+      when not defined(release):
+        logging.debug("  -> $1" % path)
+      return
+
+  return (TCActionSend, status, none[seq[(string, string)]](), "", true)
+
+proc handleRequestSlow(
+  jes: Jester,
+  req: Request,
+  respDataFut: Future[ResponseData] | ResponseData,
+  dispatchedError: bool
+): Future[void] {.async.} =
+  var dispatchedError = dispatchedError
+  var respData: ResponseData
+
+  # httpReq.send(Http200, "Hello, World!", "")
+  try:
+    when respDataFut is Future[ResponseData]:
+      respData = await respDataFut
+    else:
+      respData = respDataFut
+  except:
+    # Handle any errors by showing them in the browser.
+    # TODO: Improve the look of this.
+    let exc = getCurrentException()
+    respData = await dispatchError(jes, req, initRouteError(exc))
+    dispatchedError = true
+
+  # TODO: Put this in a custom matcher?
+  if not respData.matched:
+    respData = await handleFileRequest(jes, req)
+
+  case respData.action
+  of TCActionSend:
+    if (respData.code.is4xx or respData.code.is5xx) and
+        not dispatchedError and respData.content.len == 0:
+      respData = await dispatchError(jes, req, initRouteError(respData))
+
+    statusContent(
+      req,
+      respData.code,
+      respData.content,
+      respData.headers
+    )
+  else:
+    when not defined(release):
+      logging.debug("  $1" % [$respData.action])
+
+  # Cannot close the client socket. AsyncHttpServer may be keeping it alive.
+
+proc handleRequest(jes: Jester, httpReq: NativeRequest): Future[void] =
+  var req = initRequest(httpReq, jes.settings)
+  try:
+    when not defined(release):
+      logging.debug("$1 $2" % [$req.reqMethod, req.pathInfo])
+
+    if likely(jes.matchers.len == 1 and not jes.matchers[0].async):
+      let respData = jes.matchers[0].syncProc(req)
+      if likely(respData.matched):
+        statusContent(
+          req,
+          respData.code,
+          respData.content,
+          respData.headers
+        )
+      else:
+        return handleRequestSlow(jes, req, respData, false)
+    else:
+      return handleRequestSlow(jes, req, dispatch(jes, req), false)
+  except:
+    let exc = getCurrentException()
+    let respDataFut = dispatchError(jes, req, initRouteError(exc))
+    return handleRequestSlow(jes, req, respDataFut, true)
+
+proc newSettings*(
+  port = Port(5000), staticDir = getCurrentDir() / "public",
+  appName = "", bindAddr = "", reusePort = false,
+  futureErrorHandler: proc (fut: Future[void]) {.closure, gcsafe.} = nil
+): Settings =
+  result = Settings(
+    staticDir: staticDir,
+    appName: appName,
+    port: port,
+    bindAddr: bindAddr,
+    reusePort: reusePort,
+    futureErrorHandler: futureErrorHandler
+  )
+
+proc register*(self: var Jester, matcher: MatchProc) =
+  ## Adds the specified matcher procedure to the specified Jester instance.
+  self.matchers.add(
+    Matcher(
+      async: true,
+      asyncProc: matcher
+    )
+  )
+
+proc register*(self: var Jester, matcher: MatchProcSync) =
+  ## Adds the specified matcher procedure to the specified Jester instance.
+  self.matchers.add(
+    Matcher(
+      async: false,
+      syncProc: matcher
+    )
+  )
+
+proc register*(self: var Jester, errorHandler: ErrorProc) =
+  ## Adds the specified error handler procedure to the specified Jester instance.
+  self.errorHandlers.add(errorHandler)
+
+proc initJester*(
+  settings: Settings = newSettings()
+): Jester =
+  result.settings = settings
+  result.settings.mimes = newMimetypes()
+  result.matchers = @[]
+  result.errorHandlers = @[]
+
+proc initJester*(
+  matcher: MatchProc,
+  settings: Settings = newSettings()
+): Jester =
+  result = initJester(settings)
+  result.register(matcher)
+
+proc initJester*(
+  matcher: MatchProcSync, # TODO: Annoying nim bug: `MatchProc | MatchProcSync` doesn't work.
+  settings: Settings = newSettings()
+): Jester =
+  result = initJester(settings)
+  result.register(matcher)
+
+proc serve*(
+  self: var Jester
+) =
+  ## Creates a new async http server instance and registers
+  ## it with the dispatcher.
+  ##
+  ## The event loop is executed by this function, so it will block forever.
+
+  # Ensure we have at least one logger enabled, defaulting to console.
+  if logging.getHandlers().len == 0:
+    addHandler(logging.newConsoleLogger())
+    setLogFilter(when defined(release): lvlInfo else: lvlDebug)
+
+  if self.settings.bindAddr.len > 0:
+    logging.info("Jester is making jokes at http://$1:$2$3" %
+      [
+        self.settings.bindAddr, $self.settings.port, self.settings.appName
+      ]
+    )
+  else:
+    logging.info("Jester is making jokes at http://0.0.0.0:$1$2" %
+                 [$self.settings.port, self.settings.appName])
+
+  var jes = self
+  when useHttpBeast:
+    run(
+      proc (req: httpbeast.Request): Future[void] =
+        result = handleRequest(jes, req),
+      httpbeast.initSettings(self.settings.port, self.settings.bindAddr)
+    )
+  else:
+    self.httpServer = newAsyncHttpServer(reusePort=self.settings.reusePort)
+    let serveFut = self.httpServer.serve(
+      self.settings.port,
+      proc (req: asynchttpserver.Request): Future[void] {.gcsafe, closure.} =
+        result = handleRequest(jes, req),
+      self.settings.bindAddr)
+    if not self.settings.futureErrorHandler.isNil:
+      serveFut.callback = self.settings.futureErrorHandler
+    else:
+      asyncCheck serveFut
+    runForever()
+
+template resp*(code: HttpCode,
+               headers: openarray[tuple[key, value: string]],
+               content: string): typed =
+  ## Sets ``(code, headers, content)`` as the response.
+  bind TCActionSend, newHttpHeaders
+  result = (TCActionSend, code, headers.newHttpHeaders.some(), content, true)
+  break route
+
+template setHeader(headers: var Option[RawHeaders], key, value: string): typed =
+  bind isNone
+  if isNone(headers):
+    headers = some(@({key: value}))
+  else:
+    block outer:
+      # Overwrite key if it exists.
+      var h = headers.get()
+      for i in 0 ..< h.len:
+        if h[i][0] == key:
+          h[i][1] = value
+          headers = some(h)
+          break outer
+
+      # Add key if it doesn't exist.
+      headers = some(h & @({key: value}))
+
+template resp*(content: string, contentType = "text/html;charset=utf-8"): typed =
+  ## Sets ``content`` as the response; ``Http200`` as the status code
+  ## and ``contentType`` as the Content-Type.
+  bind TCActionSend, newHttpHeaders, strtabs.`[]=`
+  result[0] = TCActionSend
+  result[1] = Http200
+  setHeader(result[2], "Content-Type", contentType)
+  result[3] = content
+  # This will be set by our macro, so this is here for those not using it.
+  result.matched = true
+  break route
+
+template resp*(content: JsonNode): typed =
+  ## Serializes ``content`` as the response, sets ``Http200`` as status code
+  ## and "application/json" Content-Type.
+  resp($content, contentType="application/json")
+
+template resp*(code: HttpCode, content: string,
+               contentType = "text/html;charset=utf-8"): typed =
+  ## Sets ``content`` as the response; ``code`` as the status code
+  ## and ``contentType`` as the Content-Type.
+  bind TCActionSend, newHttpHeaders
+  result[0] = TCActionSend
+  result[1] = code
+  setHeader(result[2], "Content-Type", contentType)
+  result[3] = content
+  result.matched = true
+  break route
+
+template resp*(code: HttpCode): typed =
+  ## Responds with the specified ``HttpCode``. This ensures that error handlers
+  ## are called.
+  bind TCActionSend, newHttpHeaders
+  result[0] = TCActionSend
+  result[1] = code
+  result.matched = true
+  break route
+
+template redirect*(url: string): typed =
+  ## Redirects to ``url``. Returns from this request handler immediately.
+  ## Any set response headers are preserved for this request.
+  bind TCActionSend, newHttpHeaders
+  result[0] = TCActionSend
+  result[1] = Http303
+  setHeader(result[2], "Location", url)
+  result[3] = ""
+  result.matched = true
+  break route
+
+template pass*(): typed =
+  ## Skips this request handler.
+  ##
+  ## If you want to stop this request from going further use ``halt``.
+  result.action = TCActionPass
+  break outerRoute
+
+template cond*(condition: bool): typed =
+  ## If ``condition`` is ``False`` then ``pass`` will be called,
+  ## i.e. this request handler will be skipped.
+  if not condition: break outerRoute
+
+template halt*(code: HttpCode,
+               headers: openarray[tuple[key, val: string]],
+               content: string): typed =
+  ## Immediately replies with the specified request. This means any further
+  ## code will not be executed after calling this template in the current
+  ## route.
+  bind TCActionSend, newHttpHeaders
+  result[0] = TCActionSend
+  result[1] = code
+  result[2] = some(@headers)
+  result[3] = content
+  result.matched = true
+  break allRoutes
+
+template halt*(): typed =
+  ## Halts the execution of this request immediately. Returns a 404.
+  ## All previously set values are **discarded**.
+  halt(Http404, {"Content-Type": "text/html;charset=utf-8"}, error($Http404, jesterVer))
+
+template halt*(code: HttpCode): typed =
+  halt(code, {"Content-Type": "text/html;charset=utf-8"}, error($code, jesterVer))
+
+template halt*(content: string): typed =
+  halt(Http404, {"Content-Type": "text/html;charset=utf-8"}, content)
+
+template halt*(code: HttpCode, content: string): typed =
+  halt(code, {"Content-Type": "text/html;charset=utf-8"}, content)
+
+template attachment*(filename = ""): typed =
+  ## Instructs the browser that the response should be stored on disk
+  ## rather than displayed in the browser.
+  var disposition = "attachment"
+  if filename != "":
+    disposition.add("; filename=\"" & extractFilename(filename) & "\"")
+    let ext = splitFile(filename).ext
+    let contentTypeSet =
+      isSome(result[2]) and result[2].get().toTable.hasKey("Content-Type")
+    if not contentTypeSet and ext != "":
+      setHeader(result[2], "Content-Type", getMimetype(request.settings.mimes, ext))
+  setHeader(result[2], "Content-Disposition", disposition)
+
+template sendFile*(filename: string): typed =
+  ## Sends the file at the specified filename as the response.
+  result[0] = TCActionRaw
+  let sendFut = sendStaticIfExists(request, @[filename])
+  yield sendFut
+  let status = sendFut.read()
+  if status != Http200:
+    raise newException(JesterError, "Couldn't send requested file: " & filename)
+  # This will be set by our macro, so this is here for those not using it.
+  result.matched = true
+  break route
+
+template `@`*(s: string): untyped =
+  ## Retrieves the parameter ``s`` from ``request.params``. ``""`` will be
+  ## returned if parameter doesn't exist.
+  if s in params(request):
+    # TODO: Why does request.params not work? :(
+    # TODO: This is some weird bug with macros/templates, I couldn't
+    # TODO: reproduce it easily.
+    params(request)[s]
+  else:
+    ""
+
+proc setStaticDir*(request: Request, dir: string) =
+  ## Sets the directory in which Jester will look for static files. It is
+  ## ``./public`` by default.
+  ##
+  ## The files will be served like so:
+  ##
+  ## ./public/css/style.css ``->`` http://example.com/css/style.css
+  ##
+  ## (``./public`` is not included in the final URL)
+  request.settings.staticDir = dir
+
+proc getStaticDir*(request: Request): string =
+  ## Gets the directory in which Jester will look for static files.
+  ##
+  ## ``./public`` by default.
+  return request.settings.staticDir
+
+proc makeUri*(request: Request, address = "", absolute = true,
+              addScriptName = true): string =
+  ## Creates a URI based on the current request. If ``absolute`` is true it will
+  ## add the scheme (Usually 'http://'), `request.host` and `request.port`.
+  ## If ``addScriptName`` is true `request.appName` will be prepended before
+  ## ``address``.
+
+  # Check if address already starts with scheme://
+  var uri = parseUri(address)
+
+  if uri.scheme != "": return address
+  uri.path = "/"
+  uri.query = ""
+  uri.anchor = ""
+  if absolute:
+    uri.hostname = request.host
+    uri.scheme = (if request.secure: "https" else: "http")
+    if request.port != (if request.secure: 443 else: 80):
+      uri.port = $request.port
+
+  if addScriptName: uri = uri / request.appName
+  if address != "":
+    uri = uri / address
+  else:
+    uri = uri / request.pathInfo
+  return $uri
+
+template uri*(address = "", absolute = true, addScriptName = true): untyped =
+  ## Convenience template which can be used in a route.
+  request.makeUri(address, absolute, addScriptName)
+
+proc daysForward*(days: int): DateTime =
+  ## Returns a DateTime object referring to the current time plus ``days``.
+  return getTime().utc + initInterval(days = days)
+
+template setCookie*(name, value: string, expires="",
+                    sameSite: SameSite=Lax): typed =
+  ## Creates a cookie which stores ``value`` under ``name``.
+  ##
+  ## The SameSite argument determines the level of CSRF protection that
+  ## you wish to adopt for this cookie. It's set to Lax by default which
+  ## should protect you from most vulnerabilities. Note that this is only
+  ## supported by some browsers:
+  ## https://caniuse.com/#feat=same-site-cookie-attribute
+  let newCookie = makeCookie(name, value, expires)
+  if isSome(result[2]) and
+     (let headers = result[2].get(); headers.toTable.hasKey("Set-Cookie")):
+    result[2] = some(headers & @({"Set-Cookie": newCookie}))
+  else:
+    setHeader(result[2], "Set-Cookie", newCookie)
+
+template setCookie*(name, value: string, expires: DateTime,
+                    sameSite: SameSite=Lax): typed =
+  ## Creates a cookie which stores ``value`` under ``name``.
+  setCookie(name, value, format(expires, "ddd',' dd MMM yyyy HH:mm:ss 'GMT'"))
+
+proc normalizeUri*(uri: string): string =
+  ## Remove any trailing ``/``.
+  if uri[uri.len-1] == '/': result = uri[0 .. uri.len-2]
+  else: result = uri
+
+# -- Macro
+
+proc checkAction*(respData: var ResponseData): bool =
+  case respData.action
+  of TCActionSend, TCActionRaw:
+    result = true
+  of TCActionPass:
+    result = false
+  of TCActionNothing:
+    assert(false)
+
+proc skipDo(node: NimNode): NimNode {.compiletime.} =
+  if node.kind == nnkDo:
+    result = node[6]
+  else:
+    result = node
+
+proc ctParsePattern(pattern, pathPrefix: string): NimNode {.compiletime.} =
+  result = newNimNode(nnkPrefix)
+  result.add newIdentNode("@")
+  result.add newNimNode(nnkBracket)
+
+  proc addPattNode(res: var NimNode, typ, text,
+                   optional: NimNode) {.compiletime.} =
+    var objConstr = newNimNode(nnkObjConstr)
+
+    objConstr.add bindSym("Node")
+    objConstr.add newNimNode(nnkExprColonExpr).add(
+        newIdentNode("typ"), typ)
+    objConstr.add newNimNode(nnkExprColonExpr).add(
+        newIdentNode("text"), text)
+    objConstr.add newNimNode(nnkExprColonExpr).add(
+        newIdentNode("optional"), optional)
+
+    res[1].add objConstr
+
+  var patt = parsePattern(pattern)
+  if pathPrefix.len > 0:
+    result.addPattNode(
+      bindSym("NodeText"), # Node kind
+      newStrLitNode(pathPrefix), # Text
+      newIdentNode("false") # Optional?
+    )
+
+  for node in patt:
+    result.addPattNode(
+      case node.typ
+      of NodeText: bindSym("NodeText")
+      of NodeField: bindSym("NodeField"),
+      newStrLitNode(node.text),
+      newIdentNode(if node.optional: "true" else: "false"))
+
+template setDefaultResp*(): typed =
+  # TODO: bindSym this in the 'routes' macro and put it in each route
+  bind TCActionNothing, newHttpHeaders
+  result.action = TCActionNothing
+  result.code = Http200
+  result.content = ""
+
+template declareSettings(): typed {.dirty.} =
+  when not declaredInScope(settings):
+    var settings = newSettings()
+
+proc createJesterPattern(
+  routeNode, patternMatchSym: NimNode,
+  pathPrefix: string
+): NimNode {.compileTime.} =
+  var ctPattern = ctParsePattern(routeNode[1].strVal, pathPrefix)
+  # -> let <patternMatchSym> = <ctPattern>.match(request.path)
+  return newLetStmt(patternMatchSym,
+      newCall(bindSym"match", ctPattern, parseExpr("request.pathInfo")))
+
+proc escapeRegex(s: string): string =
+  result = ""
+  for i in s:
+    case i
+    # https://stackoverflow.com/a/400316/492186
+    of '.', '^', '$', '*', '+', '?', '(', ')', '[', '{', '\\', '|':
+      result.add('\\')
+      result.add(i)
+    else:
+      result.add(i)
+
+proc createRegexPattern(
+  routeNode, reMatchesSym, patternMatchSym: NimNode,
+  pathPrefix: string
+): NimNode {.compileTime.} =
+  # -> let <patternMatchSym> = find(request.pathInfo, <pattern>, <reMatches>)
+  var strNode = routeNode[1].copyNimTree()
+  strNode[1].strVal = escapeRegex(pathPrefix) & strNode[1].strVal
+  return newLetStmt(
+    patternMatchSym,
+    newCall(
+      bindSym"find",
+      parseExpr("request.pathInfo"),
+      strNode,
+      reMatchesSym
+    )
+  )
+
+proc determinePatternType(pattern: NimNode): MatchType {.compileTime.} =
+  case pattern.kind
+  of nnkStrLit:
+    var patt = parsePattern(pattern.strVal)
+    if patt.len == 1 and patt[0].typ == NodeText:
+      return MStatic
+    else:
+      return MSpecial
+  of nnkCallStrLit:
+    expectKind(pattern[0], nnkIdent)
+    case ($pattern[0]).normalize
+    of "re": return MRegex
+    else:
+      macros.error("Invalid pattern type: " & $pattern[0])
+  else:
+    macros.error("Unexpected node kind: " & $pattern.kind)
+
+proc createCheckActionIf(): NimNode =
+  var checkActionIf = parseExpr(
+    "if checkAction(result): result.matched = true; break routesList"
+  )
+  checkActionIf[0][0][0] = bindSym"checkAction"
+  return checkActionIf
+
+proc createGlobalMetaRoute(routeNode, dest: NimNode) {.compileTime.} =
+  ## Creates a ``before`` or ``after`` route with no pattern, i.e. one which
+  ## will be always executed.
+
+  # -> block route: <ifStmtBody>
+  var innerBlockStmt = newStmtList(
+    newNimNode(nnkBlockStmt).add(newIdentNode("route"), routeNode[1].skipDo())
+  )
+
+  # -> block outerRoute: <innerBlockStmt>
+  var blockStmt = newNimNode(nnkBlockStmt).add(
+    newIdentNode("outerRoute"), innerBlockStmt)
+  dest.add blockStmt
+
+proc createRoute(
+  routeNode, dest: NimNode, pathPrefix: string, isMetaRoute: bool = false
+) {.compileTime.} =
+  ## Creates code which checks whether the current request path
+  ## matches a route.
+  ##
+  ## The `isMetaRoute` parameter determines whether the route to be created is
+  ## one of either a ``before`` or an ``after`` route.
+
+  var patternMatchSym = genSym(nskLet, "patternMatchRet")
+
+  # Only used for Regex patterns.
+  var reMatchesSym = genSym(nskVar, "reMatches")
+  var reMatches = parseExpr("var reMatches: array[20, string]")
+  reMatches[0][0] = reMatchesSym
+  reMatches[0][1][1] = bindSym("MaxSubpatterns")
+
+  let patternType = determinePatternType(routeNode[1])
+  case patternType
+  of MStatic:
+    discard
+  of MSpecial:
+    dest.add createJesterPattern(routeNode, patternMatchSym, pathPrefix)
+  of MRegex:
+    dest.add reMatches
+    dest.add createRegexPattern(
+      routeNode, reMatchesSym, patternMatchSym, pathPrefix
+    )
+
+  var ifStmtBody = newStmtList()
+  case patternType
+  of MStatic: discard
+  of MSpecial:
+    # -> setPatternParams(request, ret.params)
+    ifStmtBody.add newCall(bindSym"setPatternParams", newIdentNode"request",
+                           newDotExpr(patternMatchSym, newIdentNode"params"))
+  of MRegex:
+    # -> setReMatches(request, <reMatchesSym>)
+    ifStmtBody.add newCall(bindSym"setReMatches", newIdentNode"request",
+                           reMatchesSym)
+
+  ifStmtBody.add routeNode[2].skipDo()
+
+  let checkActionIf =
+    if isMetaRoute:
+      parseExpr("break routesList")
+    else:
+      createCheckActionIf()
+  # -> block route: <ifStmtBody>; <checkActionIf>
+  var innerBlockStmt = newStmtList(
+    newNimNode(nnkBlockStmt).add(newIdentNode("route"), ifStmtBody),
+    checkActionIf
+  )
+
+  let ifCond =
+    case patternType
+    of MStatic:
+      infix(
+        parseExpr("request.pathInfo"),
+        "==",
+        newStrLitNode(pathPrefix & routeNode[1].strVal)
+      )
+    of MSpecial:
+      newDotExpr(patternMatchSym, newIdentNode("matched"))
+    of MRegex:
+      infix(patternMatchSym, "!=", newIntLitNode(-1))
+
+  # -> if <patternMatchSym>.matched: <innerBlockStmt>
+  var ifStmt = newIfStmt((ifCond, innerBlockStmt))
+
+  # -> block outerRoute: <ifStmt>
+  var blockStmt = newNimNode(nnkBlockStmt).add(
+    newIdentNode("outerRoute"), ifStmt)
+  dest.add blockStmt
+
+proc createError(
+  errorNode: NimNode,
+  httpCodeBranches,
+  exceptionBranches: var seq[tuple[cond, body: NimNode]]
+) =
+  if errorNode.len != 3:
+    error("Missing error condition or body.", errorNode)
+
+  let routeIdent = newIdentNode("route")
+  let outerRouteIdent = newIdentNode("outerRoute")
+  let checkActionIf = createCheckActionIf()
+  let exceptionIdent = newIdentNode("exception")
+  let errorIdent = newIdentNode("error") # TODO: Ugh. I shouldn't need these...
+  let errorCond = errorNode[1]
+  let errorBody = errorNode[2]
+  let body = quote do:
+    block `outerRouteIdent`:
+      block `routeIdent`:
+        `errorBody`
+      `checkActionIf`
+
+  case errorCond.kind
+  of nnkIdent:
+    let name = errorCond.strVal
+    if name.len == 7 and name.startsWith("Http"):
+      # HttpCode.
+      httpCodeBranches.add(
+        (
+          infix(parseExpr("error.data.code"), "==", errorCond),
+          body
+        )
+      )
+    else:
+      # Exception
+      exceptionBranches.add(
+        (
+          infix(parseExpr("error.exc"), "of", errorCond),
+          quote do:
+            let `exceptionIdent` = (ref `errorCond`)(`errorIdent`.exc)
+            `body`
+        )
+      )
+  of nnkCurly:
+    expectKind(errorCond[0], nnkInfix)
+    httpCodeBranches.add(
+      (
+        infix(parseExpr("error.data.code"), "in", errorCond),
+        body
+      )
+    )
+  else:
+    error("Expected exception type or set[HttpCode].", errorCond)
+
+const definedRoutes = CacheTable"jester.routes"
+
+proc processRoutesBody(
+  body: NimNode,
+  # For HTTP methods.
+  caseStmtGetBody,
+  caseStmtPostBody,
+  caseStmtPutBody,
+  caseStmtDeleteBody,
+  caseStmtHeadBody,
+  caseStmtOptionsBody,
+  caseStmtTraceBody,
+  caseStmtConnectBody,
+  caseStmtPatchBody: var NimNode,
+  # For `error`.
+  httpCodeBranches,
+  exceptionBranches: var seq[tuple[cond, body: NimNode]],
+  # For before/after stmts.
+  beforeStmts,
+  afterStmts: var NimNode,
+  # For other statements.
+  outsideStmts: var NimNode,
+  pathPrefix: string
+) =
+  for i in 0..<body.len:
+    case body[i].kind
+    of nnkCall:
+      let cmdName = body[i][0].`$`.normalize
+      case cmdName
+      of "before":
+        createGlobalMetaRoute(body[i], beforeStmts)
+      of "after":
+        createGlobalMetaRoute(body[i], afterStmts)
+      else:
+        outsideStmts.add(body[i])
+    of nnkCommand:
+      let cmdName = body[i][0].`$`.normalize
+      case cmdName
+      # HTTP Methods
+      of "get":
+        createRoute(body[i], caseStmtGetBody, pathPrefix)
+      of "post":
+        createRoute(body[i], caseStmtPostBody, pathPrefix)
+      of "put":
+        createRoute(body[i], caseStmtPutBody, pathPrefix)
+      of "delete":
+        createRoute(body[i], caseStmtDeleteBody, pathPrefix)
+      of "head":
+        createRoute(body[i], caseStmtHeadBody, pathPrefix)
+      of "options":
+        createRoute(body[i], caseStmtOptionsBody, pathPrefix)
+      of "trace":
+        createRoute(body[i], caseStmtTraceBody, pathPrefix)
+      of "connect":
+        createRoute(body[i], caseStmtConnectBody, pathPrefix)
+      of "patch":
+        createRoute(body[i], caseStmtPatchBody, pathPrefix)
+      # Other
+      of "error":
+        createError(body[i], httpCodeBranches, exceptionBranches)
+      of "before":
+        createRoute(body[i], beforeStmts, pathPrefix, isMetaRoute=true)
+      of "after":
+        createRoute(body[i], afterStmts, pathPrefix, isMetaRoute=true)
+      of "extend":
+        # Extend another router.
+        let extend = body[i]
+        if extend[1].kind != nnkIdent:
+          error("Expected identifier.", extend[1])
+
+        let prefix =
+          if extend.len > 1:
+            extend[2].strVal
+          else:
+            ""
+        if prefix.len != 0 and prefix[0] != '/':
+          error("Path prefix for extended route must start with '/'", extend[2])
+
+        processRoutesBody(
+          definedRoutes[extend[1].strVal],
+          caseStmtGetBody,
+          caseStmtPostBody,
+          caseStmtPutBody,
+          caseStmtDeleteBody,
+          caseStmtHeadBody,
+          caseStmtOptionsBody,
+          caseStmtTraceBody,
+          caseStmtConnectBody,
+          caseStmtPatchBody,
+          httpCodeBranches,
+          exceptionBranches,
+          beforeStmts,
+          afterStmts,
+          outsideStmts,
+          pathPrefix & prefix
+        )
+      else:
+        outsideStmts.add(body[i])
+    of nnkCommentStmt:
+      discard
+    of nnkPragma:
+      if body[i][0].strVal.normalize notin ["async", "sync"]:
+        outsideStmts.add(body[i])
+    else:
+      outsideStmts.add(body[i])
+
+type
+  NeedsAsync = enum
+    ImplicitTrue, ImplicitFalse, ExplicitTrue, ExplicitFalse
+proc needsAsync(node: NimNode): NeedsAsync =
+  result = ImplicitFalse
+  case node.kind
+  of nnkCommand, nnkCall:
+    if node[0].kind == nnkIdent:
+      case node[0].strVal.normalize
+      of "await", "sendfile":
+        return ImplicitTrue
+      of "resp", "halt", "attachment", "pass", "redirect", "cond", "get",
+         "post", "patch", "delete":
+        # This is just a simple heuristic. It's by no means meant to be
+        # exhaustive.
+        discard
+      else:
+        return ImplicitTrue
+  of nnkYieldStmt:
+    return ImplicitTrue
+  of nnkPragma:
+    if node[0].kind == nnkIdent:
+      case node[0].strVal.normalize
+      of "sync":
+        return ExplicitFalse
+      of "async":
+        return ExplicitTrue
+      else: discard
+  else: discard
+
+  for c in node:
+    let r = needsAsync(c)
+    if r in {ImplicitTrue, ExplicitTrue, ExplicitFalse}: return r
+
+proc routesEx(name: string, body: NimNode): NimNode =
+  # echo(treeRepr(body))
+  # echo(treeRepr(name))
+
+  # Save this route's body so that it can be incorporated into another route.
+  definedRoutes[name] = body.copyNimTree
+
+  result = newStmtList()
+
+  # -> declareSettings()
+  result.add newCall(bindSym"declareSettings")
+
+  var outsideStmts = newStmtList()
+
+  var matchBody = newNimNode(nnkStmtList)
+  let setDefaultRespIdent = bindSym"setDefaultResp"
+  matchBody.add newCall(setDefaultRespIdent)
+  # TODO: This diminishes the performance. Would be nice to only include it
+  # TODO: when setPatternParams or setReMatches is used.
+  matchBody.add parseExpr("var request = request")
+
+  # HTTP router case statement nodes:
+  var caseStmt = newNimNode(nnkCaseStmt)
+  caseStmt.add parseExpr("request.reqMethod")
+
+  var caseStmtGetBody = newNimNode(nnkStmtList)
+  var caseStmtPostBody = newNimNode(nnkStmtList)
+  var caseStmtPutBody = newNimNode(nnkStmtList)
+  var caseStmtDeleteBody = newNimNode(nnkStmtList)
+  var caseStmtHeadBody = newNimNode(nnkStmtList)
+  var caseStmtOptionsBody = newNimNode(nnkStmtList)
+  var caseStmtTraceBody = newNimNode(nnkStmtList)
+  var caseStmtConnectBody = newNimNode(nnkStmtList)
+  var caseStmtPatchBody = newNimNode(nnkStmtList)
+
+  # Error handler nodes:
+  var httpCodeBranches: seq[tuple[cond, body: NimNode]] = @[]
+  var exceptionBranches: seq[tuple[cond, body: NimNode]] = @[]
+
+  # Before/After nodes:
+  var beforeRoutes = newStmtList()
+  var afterRoutes = newStmtList()
+
+  processRoutesBody(
+    body,
+    caseStmtGetBody,
+    caseStmtPostBody,
+    caseStmtPutBody,
+    caseStmtDeleteBody,
+    caseStmtHeadBody,
+    caseStmtOptionsBody,
+    caseStmtTraceBody,
+    caseStmtConnectBody,
+    caseStmtPatchBody,
+    httpCodeBranches,
+    exceptionBranches,
+    beforeRoutes,
+    afterRoutes,
+    outsideStmts,
+    ""
+  )
+
+  var ofBranchGet = newNimNode(nnkOfBranch)
+  ofBranchGet.add newIdentNode("HttpGet")
+  ofBranchGet.add caseStmtGetBody
+  caseStmt.add ofBranchGet
+
+  var ofBranchPost = newNimNode(nnkOfBranch)
+  ofBranchPost.add newIdentNode("HttpPost")
+  ofBranchPost.add caseStmtPostBody
+  caseStmt.add ofBranchPost
+
+  var ofBranchPut = newNimNode(nnkOfBranch)
+  ofBranchPut.add newIdentNode("HttpPut")
+  ofBranchPut.add caseStmtPutBody
+  caseStmt.add ofBranchPut
+
+  var ofBranchDelete = newNimNode(nnkOfBranch)
+  ofBranchDelete.add newIdentNode("HttpDelete")
+  ofBranchDelete.add caseStmtDeleteBody
+  caseStmt.add ofBranchDelete
+
+  var ofBranchHead = newNimNode(nnkOfBranch)
+  ofBranchHead.add newIdentNode("HttpHead")
+  ofBranchHead.add caseStmtHeadBody
+  caseStmt.add ofBranchHead
+
+  var ofBranchOptions = newNimNode(nnkOfBranch)
+  ofBranchOptions.add newIdentNode("HttpOptions")
+  ofBranchOptions.add caseStmtOptionsBody
+  caseStmt.add ofBranchOptions
+
+  var ofBranchTrace = newNimNode(nnkOfBranch)
+  ofBranchTrace.add newIdentNode("HttpTrace")
+  ofBranchTrace.add caseStmtTraceBody
+  caseStmt.add ofBranchTrace
+
+  var ofBranchConnect = newNimNode(nnkOfBranch)
+  ofBranchConnect.add newIdentNode("HttpConnect")
+  ofBranchConnect.add caseStmtConnectBody
+  caseStmt.add ofBranchConnect
+
+  var ofBranchPatch = newNimNode(nnkOfBranch)
+  ofBranchPatch.add newIdentNode("HttpPatch")
+  ofBranchPatch.add caseStmtPatchBody
+  caseStmt.add ofBranchPatch
+
+  # Wrap the routes inside ``routesList`` blocks accordingly, and add them to
+  # the `match` procedure body.
+  let routesListIdent = newIdentNode("routesList")
+  matchBody.add(
+    quote do:
+      block `routesListIdent`:
+        `beforeRoutes`
+  )
+
+  matchBody.add(
+    quote do:
+      block `routesListIdent`:
+        `caseStmt`
+  )
+
+  matchBody.add(
+    quote do:
+      block `routesListIdent`:
+        `afterRoutes`
+  )
+
+  let matchIdent = newIdentNode(name)
+  let reqIdent = newIdentNode("request")
+  let needsAsync = needsAsync(body)
+  case needsAsync
+  of ImplicitFalse, ExplicitFalse:
+    hint(fmt"Synchronous route `{name}` has been optimised. Use `{{.async.}}` to change.")
+  of ImplicitTrue, ExplicitTrue:
+    hint(fmt"Asynchronous route: {name}.")
+  var matchProc =
+    if needsAsync in {ImplicitTrue, ExplicitTrue}:
+      quote do:
+        proc `matchIdent`(
+          `reqIdent`: Request
+        ): Future[ResponseData] {.async, gcsafe.} =
+          discard
+    else:
+      quote do:
+        proc `matchIdent`(
+          `reqIdent`: Request
+        ): ResponseData {.gcsafe.} =
+          discard
+
+  # The following `block` is for `halt`. (`return` didn't work :/)
+  let allRoutesBlock = newTree(
+    nnkBlockStmt,
+    newIdentNode("allRoutes"),
+    matchBody
+  )
+  matchProc[6] = newTree(nnkStmtList, allRoutesBlock)
+  result.add(outsideStmts)
+  result.add(matchProc)
+
+  # Error handler proc
+  let errorHandlerIdent = newIdentNode(name & "ErrorHandler")
+  let errorIdent = newIdentNode("error")
+  let exceptionIdent = newIdentNode("exception")
+  let resultIdent = newIdentNode("result")
+  var errorHandlerProc = quote do:
+    proc `errorHandlerIdent`(
+      `reqIdent`: Request, `errorIdent`: RouteError
+    ): Future[ResponseData] {.gcsafe, async.} =
+      block `routesListIdent`:
+        `setDefaultRespIdent`()
+        case `errorIdent`.kind
+        of RouteException:
+          discard
+        of RouteCode:
+          discard
+  if exceptionBranches.len != 0:
+    var stmts = newStmtList()
+    for branch in exceptionBranches:
+      stmts.add(newIfStmt(branch))
+    errorHandlerProc[6][0][1][^1][1][1][0] = stmts
+  if httpCodeBranches.len > 1:
+    var stmts = newStmtList()
+    for branch in httpCodeBranches:
+      stmts.add(newIfStmt(branch))
+    errorHandlerProc[6][0][1][^1][2][1][0] = stmts
+  result.add(errorHandlerProc)
+
+  # TODO: Replace `body`, `headers`, `code` in routes with `result[i]` to
+  # get these shortcuts back without sacrificing usability.
+  # TODO2: Make sure you replace what `guessAction` used to do for this.
+
+  # echo toStrLit(result)
+  # echo treeRepr(result)
+
+macro routes*(body: untyped): typed =
+  result = routesEx("match", body)
+  let jesIdent = genSym(nskVar, "jes")
+  let matchIdent = newIdentNode("match")
+  let errorHandlerIdent = newIdentNode("matchErrorHandler")
+  let settingsIdent = newIdentNode("settings")
+  result.add(
+    quote do:
+      var `jesIdent` = initJester(`matchIdent`, `settingsIdent`)
+      `jesIdent`.register(`errorHandlerIdent`)
+  )
+  result.add(
+    quote do:
+      serve(`jesIdent`)
+  )
+
+macro router*(name: untyped, body: untyped): typed =
+  if name.kind != nnkIdent:
+    error("Need an ident.", name)
+
+  routesEx($name.ident, body)
+
+macro settings*(body: untyped): typed =
+  #echo(treeRepr(body))
+  expectKind(body, nnkStmtList)
+
+  result = newStmtList()
+
+  # var settings = newSettings()
+  let settingsIdent = newIdentNode("settings")
+  result.add newVarStmt(settingsIdent, newCall("newSettings"))
+
+  for asgn in body.children:
+    expectKind(asgn, nnkAsgn)
+    result.add newAssignment(newDotExpr(settingsIdent, asgn[0]), asgn[1])
diff --git a/tests/deps/jester-#head/jester.nimble b/tests/deps/jester-#head/jester.nimble
new file mode 100644
index 000000000..08e990bd7
--- /dev/null
+++ b/tests/deps/jester-#head/jester.nimble
@@ -0,0 +1,22 @@
+# Package
+
+version       = "0.4.0" # Be sure to update jester.jesterVer too!
+author        = "Dominik Picheta"
+description   = "A sinatra-like web framework for Nim."
+license       = "MIT"
+
+skipFiles = @["todo.markdown"]
+skipDirs = @["tests"]
+
+# Deps
+
+requires "nim >= 0.18.1"
+
+when not defined(windows):
+  requires "httpbeast >= 0.2.0"
+
+# For tests
+requires "asynctools"
+
+task test, "Runs the test suite.":
+  exec "nimble c -y -r tests/tester"
\ No newline at end of file
diff --git a/tests/deps/jester-#head/jester/patterns.nim b/tests/deps/jester-#head/jester/patterns.nim
new file mode 100644
index 000000000..52b0d3a15
--- /dev/null
+++ b/tests/deps/jester-#head/jester/patterns.nim
@@ -0,0 +1,141 @@
+# Copyright (C) 2012-2018 Dominik Picheta
+# MIT License - Look at license.txt for details.
+import parseutils, tables
+type
+  NodeType* = enum
+    NodeText, NodeField
+  Node* = object
+    typ*: NodeType
+    text*: string
+    optional*: bool
+  
+  Pattern* = seq[Node]
+
+#/show/@id/?
+proc parsePattern*(pattern: string): Pattern =
+  result = @[]
+  template addNode(result: var Pattern, theT: NodeType, theText: string,
+                   isOptional: bool): typed =
+    block:
+      var newNode: Node
+      newNode.typ = theT
+      newNode.text = theText
+      newNode.optional = isOptional
+      result.add(newNode)
+
+  template `{}`(s: string, i: int): char =
+    if i >= len(s):
+      '\0'
+    else:
+      s[i]
+
+  var i = 0
+  var text = ""
+  while i < pattern.len():
+    case pattern[i]
+    of '@':
+      # Add the stored text.
+      if text != "":
+        result.addNode(NodeText, text, false)
+        text = ""
+      # Parse named parameter.
+      inc(i) # Skip @
+      var nparam = ""
+      i += pattern.parseUntil(nparam, {'/', '?'}, i)
+      var optional = pattern{i} == '?'
+      result.addNode(NodeField, nparam, optional)
+      if pattern{i} == '?': inc(i) # Only skip ?. / should not be skipped.
+    of '?':
+      var optionalChar = text[^1]
+      setLen(text, text.len-1) # Truncate ``text``.
+      # Add the stored text.
+      if text != "":
+        result.addNode(NodeText, text, false)
+        text = ""
+      # Add optional char.
+      inc(i) # Skip ?
+      result.addNode(NodeText, $optionalChar, true)
+    of '\\':
+      inc i # Skip \
+      if pattern[i] notin {'?', '@', '\\'}:
+        raise newException(ValueError, 
+                "This character does not require escaping: " & pattern[i])
+      text.add(pattern{i})
+      inc i # Skip ``pattern[i]``
+    else:
+      text.add(pattern{i})
+      inc(i)
+  
+  if text != "":
+    result.addNode(NodeText, text, false)
+
+proc findNextText(pattern: Pattern, i: int, toNode: var Node): bool =
+  ## Finds the next NodeText in the pattern, starts looking from ``i``.
+  result = false
+  for n in i..pattern.len()-1:
+    if pattern[n].typ == NodeText:
+      toNode = pattern[n]
+      return true
+
+proc check(n: Node, s: string, i: int): bool =
+  let cutTo = (n.text.len-1)+i
+  if cutTo > s.len-1: return false
+  return s.substr(i, cutTo) == n.text
+
+proc match*(pattern: Pattern, s: string):
+      tuple[matched: bool, params: Table[string, string]] =
+  var i = 0 # Location in ``s``.
+
+  result.matched = true
+  result.params = initTable[string, string]()
+
+  for ncount, node in pattern:
+    case node.typ
+    of NodeText:
+      if node.optional:
+        if check(node, s, i):
+          inc(i, node.text.len) # Skip over this optional character.
+        else:
+          # If it's not there, we have nothing to do. It's optional after all.
+          discard
+      else:
+        if check(node, s, i):
+          inc(i, node.text.len) # Skip over this
+        else:
+          # No match.
+          result.matched = false
+          return
+    of NodeField:
+      var nextTxtNode: Node
+      var stopChar = '/'
+      if findNextText(pattern, ncount, nextTxtNode):
+        stopChar = nextTxtNode.text[0]
+      var matchNamed = ""
+      i += s.parseUntil(matchNamed, stopChar, i)
+      result.params[node.text] = matchNamed
+      if matchNamed == "" and not node.optional:
+        result.matched = false
+        return
+
+  if s.len != i:
+    result.matched = false
+
+when isMainModule:
+  let f = parsePattern("/show/@id/test/@show?/?")
+  doAssert match(f, "/show/12/test/hallo/").matched
+  doAssert match(f, "/show/2131726/test/jjjuuwąąss").matched
+  doAssert(not match(f, "/").matched)
+  doAssert(not match(f, "/show//test//").matched)
+  doAssert(match(f, "/show/asd/test//").matched)
+  doAssert(not match(f, "/show/asd/asd/test/jjj/").matched)
+  doAssert(match(f, "/show/@łę¶ŧ←/test/asd/").params["id"] == "@łę¶ŧ←")
+
+  let f2 = parsePattern("/test42/somefile.?@ext?/?")
+  doAssert(match(f2, "/test42/somefile/").params["ext"] == "")
+  doAssert(match(f2, "/test42/somefile.txt").params["ext"] == "txt")
+  doAssert(match(f2, "/test42/somefile.txt/").params["ext"] == "txt")
+  
+  let f3 = parsePattern(r"/test32/\@\\\??")
+  doAssert(match(f3, r"/test32/@\").matched)
+  doAssert(not match(f3, r"/test32/@\\").matched)
+  doAssert(match(f3, r"/test32/@\?").matched)
diff --git a/tests/deps/jester-#head/jester/private/errorpages.nim b/tests/deps/jester-#head/jester/private/errorpages.nim
new file mode 100644
index 000000000..d1e695040
--- /dev/null
+++ b/tests/deps/jester-#head/jester/private/errorpages.nim
@@ -0,0 +1,19 @@
+# Copyright (C) 2012 Dominik Picheta
+# MIT License - Look at license.txt for details.
+import htmlgen
+proc error*(err, jesterVer: string): string =
+   return html(head(title(err)),
+               body(h1(err),
+                    "<hr/>",
+                    p("Jester " & jesterVer),
+                    style = "text-align: center;"
+               ),
+               xmlns="http://www.w3.org/1999/xhtml")
+
+proc routeException*(error: string, jesterVer: string): string =
+  return html(head(title("Jester route exception")),
+              body(
+                h1("An error has occured in one of your routes."),
+                p(b("Detail: "), error)
+              ),
+             xmlns="http://www.w3.org/1999/xhtml")
diff --git a/tests/deps/jester-#head/jester/private/utils.nim b/tests/deps/jester-#head/jester/private/utils.nim
new file mode 100644
index 000000000..66f0b37a6
--- /dev/null
+++ b/tests/deps/jester-#head/jester/private/utils.nim
@@ -0,0 +1,195 @@
+# Copyright (C) 2012 Dominik Picheta
+# MIT License - Look at license.txt for details.
+import parseutils, strtabs, strutils, tables, net, mimetypes, asyncdispatch, os
+from cgi import decodeUrl
+
+const
+  useHttpBeast* = false # not defined(windows) and not defined(useStdLib)
+
+type
+  MultiData* = OrderedTable[string, tuple[fields: StringTableRef, body: string]]
+
+  Settings* = ref object
+    staticDir*: string # By default ./public
+    appName*: string
+    mimes*: MimeDb
+    port*: Port
+    bindAddr*: string
+    reusePort*: bool
+    futureErrorHandler*: proc (fut: Future[void]) {.closure, gcsafe.}
+
+  JesterError* = object of Exception
+
+proc parseUrlQuery*(query: string, result: var Table[string, string])
+    {.deprecated: "use stdlib".} =
+  var i = 0
+  i = query.skip("?")
+  while i < query.len()-1:
+    var key = ""
+    var val = ""
+    i += query.parseUntil(key, '=', i)
+    if query[i] != '=':
+      raise newException(ValueError, "Expected '=' at " & $i &
+                         " but got: " & $query[i])
+    inc(i) # Skip =
+    i += query.parseUntil(val, '&', i)
+    inc(i) # Skip &
+    result[decodeUrl(key)] = decodeUrl(val)
+
+template parseContentDisposition(): typed =
+  var hCount = 0
+  while hCount < hValue.len()-1:
+    var key = ""
+    hCount += hValue.parseUntil(key, {';', '='}, hCount)
+    if hValue[hCount] == '=':
+      var value = hvalue.captureBetween('"', start = hCount)
+      hCount += value.len+2
+      inc(hCount) # Skip ;
+      hCount += hValue.skipWhitespace(hCount)
+      if key == "name": name = value
+      newPart[0][key] = value
+    else:
+      inc(hCount)
+      hCount += hValue.skipWhitespace(hCount)
+
+proc parseMultiPart*(body: string, boundary: string): MultiData =
+  result = initOrderedTable[string, tuple[fields: StringTableRef, body: string]]()
+  var mboundary = "--" & boundary
+
+  var i = 0
+  var partsLeft = true
+  while partsLeft:
+    var firstBoundary = body.skip(mboundary, i)
+    if firstBoundary == 0:
+      raise newException(ValueError, "Expected boundary. Got: " & body.substr(i, i+25))
+    i += firstBoundary
+    i += body.skipWhitespace(i)
+
+    # Headers
+    var newPart: tuple[fields: StringTableRef, body: string] = ({:}.newStringTable, "")
+    var name = ""
+    while true:
+      if body[i] == '\c':
+        inc(i, 2) # Skip \c\L
+        break
+      var hName = ""
+      i += body.parseUntil(hName, ':', i)
+      if body[i] != ':':
+        raise newException(ValueError, "Expected : in headers.")
+      inc(i) # Skip :
+      i += body.skipWhitespace(i)
+      var hValue = ""
+      i += body.parseUntil(hValue, {'\c', '\L'}, i)
+      if toLowerAscii(hName) == "content-disposition":
+        parseContentDisposition()
+      newPart[0][hName] = hValue
+      i += body.skip("\c\L", i) # Skip *one* \c\L
+
+    # Parse body.
+    while true:
+      if body[i] == '\c' and body[i+1] == '\L' and
+         body.skip(mboundary, i+2) != 0:
+        if body.skip("--", i+2+mboundary.len) != 0:
+          partsLeft = false
+          break
+        break
+      else:
+        newPart[1].add(body[i])
+      inc(i)
+    i += body.skipWhitespace(i)
+
+    result.add(name, newPart)
+
+proc parseMPFD*(contentType: string, body: string): MultiData =
+  var boundaryEqIndex = contentType.find("boundary=")+9
+  var boundary = contentType.substr(boundaryEqIndex, contentType.len()-1)
+  return parseMultiPart(body, boundary)
+
+proc parseCookies*(s: string): Table[string, string] =
+  ## parses cookies into a string table.
+  ##
+  ## The proc is meant to parse the Cookie header set by a client, not the
+  ## "Set-Cookie" header set by servers.
+
+  result = initTable[string, string]()
+  var i = 0
+  while true:
+    i += skipWhile(s, {' ', '\t'}, i)
+    var keystart = i
+    i += skipUntil(s, {'='}, i)
+    var keyend = i-1
+    if i >= len(s): break
+    inc(i) # skip '='
+    var valstart = i
+    i += skipUntil(s, {';'}, i)
+    result[substr(s, keystart, keyend)] = substr(s, valstart, i-1)
+    if i >= len(s): break
+    inc(i) # skip ';'
+
+type
+  SameSite* = enum
+    None, Lax, Strict
+
+proc makeCookie*(key, value, expires: string, domain = "", path = "",
+                 secure = false, httpOnly = false,
+                 sameSite = Lax): string =
+  result = ""
+  result.add key & "=" & value
+  if domain != "": result.add("; Domain=" & domain)
+  if path != "": result.add("; Path=" & path)
+  if expires != "": result.add("; Expires=" & expires)
+  if secure: result.add("; Secure")
+  if httpOnly: result.add("; HttpOnly")
+  if sameSite != None:
+    result.add("; SameSite=" & $sameSite)
+
+when not declared(tables.getOrDefault):
+  template getOrDefault*(tab, key): untyped = tab[key]
+
+when not declared(normalizePath) and not declared(normalizedPath):
+  proc normalizePath*(path: var string) =
+    ## Normalize a path.
+    ##
+    ## Consecutive directory separators are collapsed, including an initial double slash.
+    ##
+    ## On relative paths, double dot (..) sequences are collapsed if possible.
+    ## On absolute paths they are always collapsed.
+    ##
+    ## Warning: URL-encoded and Unicode attempts at directory traversal are not detected.
+    ## Triple dot is not handled.
+    let isAbs = isAbsolute(path)
+    var stack: seq[string] = @[]
+    for p in split(path, {DirSep}):
+      case p
+      of "", ".":
+        continue
+      of "..":
+        if stack.len == 0:
+          if isAbs:
+            discard  # collapse all double dots on absoluta paths
+          else:
+            stack.add(p)
+        elif stack[^1] == "..":
+          stack.add(p)
+        else:
+          discard stack.pop()
+      else:
+        stack.add(p)
+
+    if isAbs:
+      path = DirSep & join(stack, $DirSep)
+    elif stack.len > 0:
+      path = join(stack, $DirSep)
+    else:
+      path = "."
+
+  proc normalizedPath*(path: string): string =
+    ## Returns a normalized path for the current OS. See `<#normalizePath>`_
+    result = path
+    normalizePath(result)
+
+when isMainModule:
+  var r = {:}.newStringTable
+  parseUrlQuery("FirstName=Mickey", r)
+  echo r
+
diff --git a/tests/deps/jester-#head/jester/request.nim b/tests/deps/jester-#head/jester/request.nim
new file mode 100644
index 000000000..1b837d728
--- /dev/null
+++ b/tests/deps/jester-#head/jester/request.nim
@@ -0,0 +1,184 @@
+import uri, cgi, tables, logging, strutils, re, options
+
+import jester/private/utils
+
+when useHttpBeast:
+  import httpbeast except Settings
+  import options, httpcore
+
+  type
+    NativeRequest* = httpbeast.Request
+else:
+  import asynchttpserver
+
+  type
+    NativeRequest* = asynchttpserver.Request
+
+type
+  Request* = object
+    req: NativeRequest
+    patternParams: Option[Table[string, string]]
+    reMatches: array[MaxSubpatterns, string]
+    settings*: Settings
+
+proc body*(req: Request): string =
+  ## Body of the request, only for POST.
+  ##
+  ## You're probably looking for ``formData``
+  ## instead.
+  when useHttpBeast:
+    req.req.body.get("")
+  else:
+    req.req.body
+
+proc headers*(req: Request): HttpHeaders =
+  ## Headers received with the request.
+  ## Retrieving these is case insensitive.
+  when useHttpBeast:
+    if req.req.headers.isNone:
+      newHttpHeaders()
+    else:
+      req.req.headers.get()
+  else:
+    req.req.headers
+
+proc path*(req: Request): string =
+  ## Path of request without the query string.
+  when useHttpBeast:
+    let p = req.req.path.get("")
+    let queryStart = p.find('?')
+    if unlikely(queryStart != -1):
+      return p[0 .. queryStart-1]
+    else:
+      return p
+  else:
+    let u = req.req.url
+    return u.path
+
+proc reqMethod*(req: Request): HttpMethod =
+  ## Request method, eg. HttpGet, HttpPost
+  when useHttpBeast:
+    req.req.httpMethod.get()
+  else:
+    req.req.reqMethod
+proc reqMeth*(req: Request): HttpMethod {.deprecated.} =
+  req.reqMethod
+
+proc ip*(req: Request): string =
+  ## IP address of the requesting client.
+  when useHttpBeast:
+    result = req.req.ip
+  else:
+    result = req.req.hostname
+
+  let headers = req.headers
+  if headers.hasKey("REMOTE_ADDR"):
+    result = headers["REMOTE_ADDR"]
+  if headers.hasKey("x-forwarded-for"):
+    result = headers["x-forwarded-for"]
+
+proc params*(req: Request): Table[string, string] =
+  ## Parameters from the pattern and the query string.
+  if req.patternParams.isSome():
+    result = req.patternParams.get()
+  else:
+    result = initTable[string, string]()
+
+  when useHttpBeast:
+    let query = req.req.path.get("").parseUri().query
+  else:
+    let query = req.req.url.query
+
+  try:
+    for key, val in cgi.decodeData(query):
+      result[key] = val
+  except CgiError:
+    logging.warn("Incorrect query. Got: $1" % [query])
+
+  let contentType = req.headers.getOrDefault("Content-Type")
+  if contentType.startswith("application/x-www-form-urlencoded"):
+    try:
+      parseUrlQuery(req.body, result)
+    except:
+      logging.warn("Could not parse URL query.")
+
+proc formData*(req: Request): MultiData =
+  let contentType = req.headers.getOrDefault("Content-Type")
+  if contentType.startsWith("multipart/form-data"):
+    result = parseMPFD(contentType, req.body)
+
+proc matches*(req: Request): array[MaxSubpatterns, string] =
+  req.reMatches
+
+proc secure*(req: Request): bool =
+  if req.headers.hasKey("x-forwarded-proto"):
+    let proto = req.headers["x-forwarded-proto"]
+    case proto.toLowerAscii()
+    of "https":
+      result = true
+    of "http":
+      result = false
+    else:
+      logging.warn("Unknown x-forwarded-proto ", proto)
+
+proc port*(req: Request): int =
+  if (let p = req.headers.getOrDefault("SERVER_PORT"); p != ""):
+    result = p.parseInt
+  else:
+    result = if req.secure: 443 else: 80
+
+proc host*(req: Request): string =
+  req.headers.getOrDefault("HOST")
+
+proc appName*(req: Request): string =
+  ## This is set by the user in ``run``, it is
+  ## overriden by the "SCRIPT_NAME" scgi
+  ## parameter.
+  req.settings.appName
+
+proc stripAppName(path, appName: string): string =
+  result = path
+  if appname.len > 0:
+    var slashAppName = appName
+    if slashAppName[0] != '/' and path[0] == '/':
+      slashAppName = '/' & slashAppName
+
+    if path.startsWith(slashAppName):
+      if slashAppName.len() == path.len:
+        return "/"
+      else:
+        return path[slashAppName.len .. path.len-1]
+    else:
+      raise newException(ValueError,
+          "Expected script name at beginning of path. Got path: " &
+           path & " script name: " & slashAppName)
+
+proc pathInfo*(req: Request): string =
+  ## This is ``.path`` without ``.appName``.
+  req.path.stripAppName(req.appName)
+
+# TODO: Can cookie keys be duplicated?
+proc cookies*(req: Request): Table[string, string] =
+  ## Cookies from the browser.
+  if (let cookie = req.headers.getOrDefault("Cookie"); cookie != ""):
+    result = parseCookies(cookie)
+  else:
+    result = initTable[string, string]()
+
+#[ Protected procs ]#
+
+proc initRequest*(req: NativeRequest, settings: Settings): Request {.inline.} =
+  Request(
+    req: req,
+    settings: settings
+  )
+
+proc getNativeReq*(req: Request): NativeRequest =
+  req.req
+
+#[ Only to be used by our route macro. ]#
+proc setPatternParams*(req: var Request, p: Table[string, string]) =
+  req.patternParams = some(p)
+
+proc setReMatches*(req: var Request, r: array[MaxSubpatterns, string]) =
+  req.reMatches = r