1350 lines
41 KiB
Nim
1350 lines
41 KiB
Nim
# 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])
|