summary refs log tree commit diff stats
path: root/tests/deps/jester-#head/jester.nim
diff options
context:
space:
mode:
Diffstat (limited to 'tests/deps/jester-#head/jester.nim')
-rw-r--r--tests/deps/jester-#head/jester.nim1350
1 files changed, 1350 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])