import asyncdispatch, asynchttpserver, macros import strutils, parseutils import cookies, uri, times, re, json import tables export asyncdispatch, asynchttpserver, cookies, uri export strutils, re export tables, json type ServerRef* = ref object port*: uint16 address*: string server*: AsyncHttpServer proc newServer* (address: string = "0.0.0.0", port: uint16 = 5000): ServerRef = return ServerRef( address: address, port: port, server: newAsyncHttpServer() ) proc cookies*(req: Request): Table[string, string] = result = initTable[string, string]() if (let cookie = req.headers.getOrDefault("Cookie"); cookie != ""): var i = 0 while true: i += skipWhile(cookie, {' ', '\t'}, i) var keystart = i i += skipUntil(cookie, {'='}, i) var keyend = i-1 if i >= len(cookie): break inc(i) # skip '=' var valstart = i i += skipUntil(cookie, {';'}, i) result[substr(cookie, keystart, keyend)] = substr(cookie, valstart, i-1) if i >= len(cookie): break inc(i) # skip ';' proc parseQuery*(data: string): Table[string, string] = result = initTable[string, string]() let data = data.split("&") for i in data: let timed = i.split("=") if timed.len > 1: result[decodeUrl(timed[0])] = decodeUrl(timed[1]) proc daysForward*(days: int): DateTime = return getTime().utc + initTimeInterval(days = days) template setCookie* (key: string, value: string, time: DateTime) = headers.add("Set-Cookie", setCookie(key, value, time, request.headers["host"], noName=true)) proc redirect* (request: Request, url: string) {.async.}= await request.respond(Http303, "", newHttpHeaders([("Location", url)])) proc respondJson* (request: Request, httpCode: HttpCode, status: string, message: string, data: JsonNode) {.async.} = let response = %* { "status": status, "msg": message, "data": data } await request.respond(httpCode, $response, newHttpHeaders([("Content-Type","application/json")])) macro pages*(server: ServerRef, body: untyped): untyped = ## This macro provides convenient page adding. ## ## `body` should be StmtList. ## page type can be: ## - ``equals`` ## - ``startsWith`` ## - ``endsWith`` ## - ``regex`` ## - ``notfound`` - this page uses without URL argument. ## ## When a new request to the server is received, variables are automatically created: ## - ``request`` - new Request. ## - ``url`` - matched URL. ## - ``equals`` - URL is request.url.path ## - ``startsWith`` - URL is text after `startswith`. ## - ``endsWith`` - URL is text before `endswith`. ## - ``regex`` - `url` param not created. ## - ``notfound`` - `url` param not created. ## - ``decoded_url`` - URL always is request.url.path var stmtlist = newStmtList() notfound_declaration = false stmtlist.add( newNimNode(nnkLetSection).add( # let urlParams: JsonNode = await parseQuery(request) newNimNode(nnkIdentDefs).add( # let decode_url: string = decodeUrl(request.url.path) ident("decoded_url"), ident("string"), newCall( "decodeUrl", newNimNode(nnkDotExpr).add( newNimNode(nnkDotExpr).add( ident("request"), ident("url") ), ident("path") ) ) ) ) ) stmtlist.add(newNimNode(nnkIfStmt)) var ifstmtlist = stmtlist[1] for i in body: # for each page in statment list. let current = if i.len == 3: $i[0] else: "equals" path = if i.len == 3: i[1] else: i[0] slist = if i.len == 3: i[2] else: i[1] if (i.kind == nnkCall and (path.kind == nnkStrLit or path.kind == nnkCallStrLit or path.kind == nnkEmpty) and slist.kind == nnkStmtList): if current == "equals": slist.insert(0, # let url: string = `path` newNimNode(nnkLetSection).add( newNimNode(nnkIdentDefs).add( ident("url"), ident("string"), path ) ) ) ifstmtlist.add( # decoded_url == `path` newNimNode(nnkElifBranch).add( newCall("==", path, ident("decoded_url")), slist ) ) elif current == "startsWith": slist.insert(0, # let url = decoded_url[`path`.len..^1] newNimNode(nnkLetSection).add( newNimNode(nnkIdentDefs).add( ident("url"), ident("string"), newCall( "[]", ident("decoded_url"), newCall("..^", newCall("len", path), newLit(1)) ) ) ) ) ifstmtlist.add( # decode_url.startsWith(`path`) newNimNode(nnkElifBranch).add( newCall("startsWith", ident("decoded_url"), path), slist ) ) elif current == "endsWith": slist.insert(0, # let url: string = decoded_url[0..^`path`.len] newNimNode(nnkLetSection).add( newNimNode(nnkIdentDefs).add( ident("url"), ident("string"), newCall( "[]", ident("decoded_url"), newCall( "..^", newLit(0), newCall("+", newLit(1), newCall("len", path)) ) ) ) ) ) ifstmtlist.add( # decode_url.endsWith(`path`) newNimNode(nnkElifBranch).add( newCall("endsWith", ident("decoded_url"), path), slist ) ) elif current == "regex": ifstmtlist.add( # decode_url.match(`path`) newNimNode(nnkElifBranch).add( newCall("match", ident("decoded_url"), path), slist)) ifstmtlist.add( newNimNode(nnkElse).add( newCall( # await request.respond(Http404, "Not found") "await", newCall("respond", ident("request"), ident("Http404"), newLit("Not found"), newCall("newHttpHeaders")) ) ) ) result = newNimNode(nnkProcDef).add( ident("receivepages"), # procedure name. newEmptyNode(), # for template and macros newEmptyNode(), # generics newNimNode(nnkFormalParams).add( # proc params newEmptyNode(), # return type newNimNode(nnkIdentDefs).add( # param ident("request"), # param name ident("Request"), # param type newEmptyNode() # param default value ) ), newNimNode(nnkPragma).add( # pragma declaration ident("async"), ident("gcsafe") ), newEmptyNode(), stmtlist) macro start*(server: ServerRef): untyped = result = quote do: echo "Server starts on http://", `server`.address, ":", `server`.port waitFor `server`.server.serve(Port(`server`.port), receivepages, `server`.address)