Hi, im writting a small API using the Akane package, on all my routes I use the exact same 2 lines wich basically returns from the main function if condition is met. To avoid manually copy pasting them I converted them to untyped templates. When the return is triggered akane errors with Exception message: Async procedure (receivepages) yielded `nil, are you await'ing a nil Future?` . Note that by copy pasting what the templates should be extended to works without any issue.
Here is a sample of the code (may be garbage):
import akane, strformat, strutils
import libs/[tlib, db, utils]
proc getCreds(request: Request): tuple =
let jsonBody = parseJson(request.body)
return (jsonBody{"username"}.getStr(), jsonBody{"password"}.getStr())
template validateBody(request:Request, username, password, failuretype:string):untyped =
if username == "" or password == "":
await request.sendJson(%*{"message": "Malformed JSON"})
warn failuretype & " failed: Username and/or password not specified"
return
proc api(port: uint16 = 5353, address:string = "localhost") =
let srv = newServer(address, port)
srv.pages:
equals("/register", HttpPost):
echo ""
info "Registering new user"
let creds = getCreds(request)
let
username = creds[0]
password = creds[1]
validateBody(request, username, password, "Registration")
if db.userExists(username): await request.sendJson(%*{"message": "Username already in use"}); warn "Account registration failed: Username already in use"; return
db.registerUser(newUser(username,hashPasswd(password),genUID()))
await request.sendJson(%*{"message": "success"})
success &"Registered user {username}"
info &"API started on {address}:{port}"
srv.start()
when isMainModule:
try:
info "Opening database"
db.openDB()
api()
except EKeyboardInterrupt:
echo ""
info "Closing database"
db.closeDB()
quit(0)
except:
db.closeDB()
stderr.writeLine(getCurrentExceptionMsg())
The second tutorial says it's just a substitution system, what I understand is that the VM replace the template calls with their respective body.
Btw, if you know any better package to make APIs (with ways to outsource the routes to their own files if possible) please share.
I'm not sure what srv.pages is doing but it's possible that its transformation is applied before your template is expanded.
The issue is that async transformation then checks the return statement to transform them into async yield (suspension) point. But the one within your template has been hidden.
Then your template is expanded after async transformation is done, and you get a cryptic error message.
TL;DR: no return or break in a template used in async procedure.
Even in terms of naming, a validate proc is expected to return a bool. So return a bool and if not validate(foo): return
_Note: There might be a case when a template is expanded early: if generic parameters are involved, due to generics requiring "early symbol resolution"_
I though of the template expanding order, but I ended up thinking that it shouldn't cause any issues.
For the a validate proc is expecter to return a bool. That's what Im doing as a "temporary fix". I still wanted to experiments with templates.
Thanks for the answer.
I usually define template without returning when I want it as statement executions:
template doStmt =
echo "do this"
template doubleInt(x: int): untyped =
x * 2
doStmt()
echo doubleInt(3)
Especially when I want the template to check for possible early return like your example (that has return keyword which intended to be executed in parent scope).
@NameUndefined, since the pages macro uses an untyped body argument, the macro expansion happens first, which transforms your code into an async procedure called receivepages that returns a future. Then your template gets expanded in that procedure without the async macro modifying the return statement and returns nil, when it expects it to be a Future. Another way you can work around this is by returning a new future with type void:
import akane, strformat, strutils, asyncdispatch
import libs/[tlib, db, utils]
proc getCreds(request: Request): tuple =
let jsonBody = parseJson(request.body)
return (jsonBody{"username"}.getStr(), jsonBody{"password"}.getStr())
template validateBody(request:Request, username, password, failuretype:string):untyped =
if username == "" or password == "":
await request.sendJson(%*{"message": "Malformed JSON"})
warn failuretype & " failed: Username and/or password not specified"
return newFuture[void]()
# other code ...
A fairly minimal recreation of this code is below and it works:
import std/macros
import asyncdispatch
proc otherAsyncProc(str: string) {.async.} =
await sleepAsync(1000)
echo str
template validateBody(str: string): untyped =
if str == "hello":
await otherAsyncProc(str)
yield newFuture[void]()
macro makeAsync(body: untyped): untyped =
return quote do:
proc receiveMessage(message: string, wait: int) {.async.} =
`body`
proc doSomething() =
makeAsync:
validateBody("hello")
asyncCheck receiveMessage("stuff", 100)
doSomething()
while asyncdispatch.hasPendingOperations():
asyncdispatch.poll()