So, I'm trying to build a small routing library for asynchttpserver. It's kind of just for learning at the moment, but it could turn into something bigger(who knows?). I've got it working somewhat the way I want it to but I'm running into a little issue:
In the following code on line #82(declaration of 'cb' proc) nimsuggest is throwing the error: "template/generic instantiation from here". I'm not actually entirely sure if this is a real error as my code compiles and seems to work properly. I used the debugger and didn't notice anything strange there either.
Even though my code is working properly, I want to make sure I don't have a lingering bug waiting to pounce.
Also, any general improvement suggestions are very welcome.
import asyncdispatch, asynchttpserver, strutils
let server = newAsyncHttpServer()
proc notFoundHandler(req: Request) {.async.} =
await req.respond(Http404, "404 Not Found")
type
Router = ref object of RootObj
routes: seq[Route]
Route = ref object of RootObj
path: string
get: proc (req: Request): Future[void]
post: proc (req: Request): Future[void]
put: proc (req: Request): Future[void]
delete: proc (req: Request): Future[void]
proc newRouter(): Router =
result = Router(
routes: @[]
)
proc newRoute(
path: string,
get, post, put, delete: proc=notFoundHandler
): Route =
result = Route(
path: path,
get: get,
post: post,
put: put,
delete: delete
)
proc register(router: Router, routes: varargs[Route]) =
for route in items(routes):
router.routes.add(route)
proc route(router: Router, req: Request) {.async.} =
for route in router.routes:
if route.path == req.url.path:
case req.reqMethod:
of HttpGet:
await route.get(req)
else: continue
await notFoundHandler(req)
let router = newRouter()
proc indexHandler(req: Request) {.async.} =
await req.respond(Http200, "This is the home page.")
proc helloWorldHandler(req: Request) {.async.} =
await req.respond(Http200, "Hello, world!")
let index = newRoute(
path="/",
get=indexHandler
)
let helloWorld = newRoute(
path="/hello/",
get=helloWorldHandler
)
router.register(index, helloWorld)
proc cb(req: Request) {.async.} =
await router.route(req)
waitFor server.serve(Port(8000), cb)
OK, so, I found an answer on the irc. Looks like the error I'm getting isn't really an error. Though it is strange that my editor is marking it as one.
Well, general suggestions are still welcome.
Thanks for the advice guys. I tried a couple of different things in order to get it to work including returning Future[void] from my async procs. Turns out the error I was getting is just the linter being a tad nit-picky and not really an error at all.
I did run into another issue which had to do with passing a proc as a default argument to a type constructor. For this issue I tried using the procvar pragma in the proc declaration, in the type signature for the type attributes which accept the proc, and in the default argument itself. @dom96 had some great advice for this on irc. Unfortunately, none of it really worked correctly. The solution that I came up with was to create a variable that aliases the proc that I wanted to pass as the default argument. It's all working as expected now. I ended up with the following:
import asynchttpserver, asyncdispatch
type
Router* = ref object of RootObj
routes: seq[Route]
Route* = ref object of RootObj
path*: string
get*: proc (req: Request): Future[void]
post*: proc (req: Request): Future[void]
put*: proc (req: Request): Future[void]
delete*: proc(req: Request): Future[void]
Response* = ref object of RootObj
request*: Request
status*: HttpCode
data*: string
headers*: HttpHeaders
proc newRouter*(): Router =
result = Router(
routes: @[]
)
proc notFoundHandler*(req: Request): Future[void] {.async.} =
await req.respond(Http404, "404 Not Found")
#[
Until I can figure out how the hell procvars work, we need
to alias the default request handler to a variable in order
for the Route constructor to accept it as a default argument.
]#
var notFoundHandlerVar = notFoundHandler
proc newRoute*(
path: string,
get, post, put, delete: proc = notFoundHandlerVar
): Route =
result = Route(
path: path,
get: get,
post: post,
put: put,
delete: delete
)
let DEFAULT_HEADERS = newHttpHeaders([
("Content-Type", "text/html"),
("Accept", "text/html"),
("Accept-Charset", "utf-8"),
("Accept-Encoding", "gzip, deflate"),
("Connection", "keep-alive")
])
proc newResponse*(
request: Request,
status: HttpCode = Http200,
data: string = "",
headers: HttpHeaders = DEFAULT_HEADERS
): Response =
result = Response(
request: request,
status: status,
data: data,
headers: headers
)
proc log*(resp: Response): void =
echo $resp.status
proc send*(resp: Response) {.async.} =
log(resp)
await resp.request.respond(
resp.status,
resp.data,
resp.headers
)
proc register*(router: Router, routes: varargs[Route]) =
for route in items(routes):
router.routes.add(route)
proc route*(router: Router, req: Request) {.async.} =
for route in router.routes:
if route.path == req.url.path:
case req.reqMethod:
of HttpGet:
await route.get(req)
else: continue
await notFoundHandler(req)
After I add the mapping for request methods other than GET, the next step is route matching with url path variables. Then I'm thinking I'll implement a more robust Response.render proc to handle rendering source code filters. The router can be used like this:
import asyncdispatch, asynchttpserver, strutils
import router
from templates import page, index, hello
let server = newAsyncHttpServer()
let myRouter = router.newRouter()
proc indexHandler(req: Request) {.async.} =
var data = page(
title="Home",
contents=index()
)
var resp = newResponse(req, data=data)
await resp.send()
proc helloWorldHandler(req: Request) {.async.} =
var data = page(
title="Hello",
contents=hello("Justin")
)
var resp = newResponse(req, data=data)
await resp.send()
let index = newRoute(
path="/",
get=indexHandler
)
let helloWorld = newRoute(
path="/hello/",
get=helloWorldHandler
)
myRouter.register(index, helloWorld)
proc log(req: Request): void =
echo "[$1] $2" % [$req.reqMethod, req.url.path]
proc cb(req: Request) {.async.} =
log(req)
await myRouter.route(req)
waitFor server.serve(Port(8000), cb)
Does anyone have anything specific they would like to see in a url routing library? I'm thinking that as I continue to learn Nim, I can build a more robust api for creating web applications.
At some point I want to try and tackle creating a simple ORM as well. I'm very familiar with Postresql and SQLite administration, but I will say that my skills in writing pure SQL are a bit lacking. Therefore, I have a feeling it will take me quite a while to get the ORM project rolling.
That workaround is odd, are you using 0.16.0? The following works for me on devel.
(by the way, I would advise against using proc for proc types, better to be explicit.)
import asynchttpserver, asyncdispatch
type
Router* = ref object of RootObj
routes: seq[Route]
Route* = ref object of RootObj
path*: string
get*: proc (req: Request): Future[void]
post*: proc (req: Request): Future[void]
put*: proc (req: Request): Future[void]
delete*: proc(req: Request): Future[void]
Response* = ref object of RootObj
request*: Request
status*: HttpCode
data*: string
headers*: HttpHeaders
proc newRouter*(): Router =
result = Router(
routes: @[]
)
proc notFoundHandler*(req: Request): Future[void] {.async.} =
await req.respond(Http404, "404 Not Found")
proc newRoute*(
path: string,
get, post, put, delete: proc (req: Request): Future[void] = notFoundHandler
): Route =
result = Route(
path: path,
get: get,
post: post,
put: put,
delete: delete
)
let DEFAULT_HEADERS = newHttpHeaders([
("Content-Type", "text/html"),
("Accept", "text/html"),
("Accept-Charset", "utf-8"),
("Accept-Encoding", "gzip, deflate"),
("Connection", "keep-alive")
])
proc newResponse*(
request: Request,
status: HttpCode = Http200,
data: string = "",
headers: HttpHeaders = DEFAULT_HEADERS
): Response =
result = Response(
request: request,
status: status,
data: data,
headers: headers
)
proc log*(resp: Response): void =
echo $resp.status
proc send*(resp: Response) {.async.} =
log(resp)
await resp.request.respond(
resp.status,
resp.data,
resp.headers
)
proc register*(router: Router, routes: varargs[Route]) =
for route in items(routes):
router.routes.add(route)
proc route*(router: Router, req: Request) {.async.} =
for route in router.routes:
if route.path == req.url.path:
case req.reqMethod:
of HttpGet:
await route.get(req)
else: continue
await notFoundHandler(req)
Cool, thanks for sharing. I like the KISS approach.
Does anyone have anything specific they would like to see in a url routing library?
I'd like to see dynamic routes, redirects and static file handling along the lines of python's bottle:
Cool, thanks for sharing. I like the KISS approach.
Admittedly, the KISS approach is probably a product of my inexperience with Nim rather than any sort of design decision.
I'd like to see dynamic routes, redirects and static file handling along the lines of python's bottle:
Dynamic routes are something I'm working on. I'm not sure if I want to use regex or native Nim types. Redirects will be implemented through the Router type. I'm going to add namespaces and names to routes ala Django's router. That way, you can generate a url for links or redirects like so: myRouter.url("namespace:name", id=2). I'm thinking that templates should have the Router object injected by default so link generation is super simple.
Static file handling tends to be more difficult than it appears on the surface based on my own personal experience working in web development for 5 years. Not so much actually serving the files, but more about creating a friendly api for configuring static file storage. I'll take a look at how Bottle does it for some ideas. I work extensively with Django and Flask, so a lot of my ideas are inspired by those frameworks. From my understanding Bottle is very similar to Flask.